diff options
author | Alex Xu <alex_y_xu@yahoo.ca> | 2013-08-22 22:45:26 -0400 |
---|---|---|
committer | Alex Xu <alex_y_xu@yahoo.ca> | 2013-08-22 22:45:26 -0400 |
commit | 718936110b9511631fa1f4396be992752bf8b719 (patch) | |
tree | a871768c06adc2959f8f0d69869532d36a95ffab /unrpyc/renpy/display/layout.py | |
parent | ece6cf9fbfdba9dac8d7bf98516a840c955a4853 (diff) | |
download | html5ks-718936110b9511631fa1f4396be992752bf8b719.tar.xz html5ks-718936110b9511631fa1f4396be992752bf8b719.zip |
include renpy
Diffstat (limited to 'unrpyc/renpy/display/layout.py')
-rw-r--r-- | unrpyc/renpy/display/layout.py | 1744 |
1 files changed, 1744 insertions, 0 deletions
diff --git a/unrpyc/renpy/display/layout.py b/unrpyc/renpy/display/layout.py new file mode 100644 index 0000000..e07e28d --- /dev/null +++ b/unrpyc/renpy/display/layout.py @@ -0,0 +1,1744 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains classes that handle layout of displayables on +# the screen. + +from renpy.display.render import render, Render +import renpy.display + + +def scale(num, base): + """ + If num is a float, multiplies it by base and returns that. Otherwise, + returns num unchanged. + """ + + if isinstance(num, float): + return num * base + else: + return num + +class Null(renpy.display.core.Displayable): + """ + :doc: disp_imagelike + + A displayable that creates an empty box on the screen. The size + of the box is controlled by `width` and `height`. This can be used + when a displayable requires a child, but no child is suitable, or + as a spacer inside a box. + + :: + + image logo spaced = HBox("logo.png", Null(width=100), "logo.png") + + """ + + def __init__(self, width=0, height=0, **properties): + super(Null, self).__init__(**properties) + self.width = width + self.height = height + + def render(self, width, height, st, at): + rv = renpy.display.render.Render(self.width, self.height) + + if self.focusable: + rv.add_focus(self, None, None, None, None, None) + + return rv + + +class Container(renpy.display.core.Displayable): + """ + This is the base class for containers that can have one or more + children. + + @ivar children: A list giving the children that have been added to + this container, in the order that they were added in. + + @ivar child: The last child added to this container. This is also + used to access the sole child in containers that can only hold + one child. + + @ivar offsets: A list giving offsets for each of our children. + It's expected that render will set this up each time it is called. + + @ivar sizes: A list giving sizes for each of our children. It's + also expected that render will set this each time it is called. + + """ + + # We indirect all list creation through this, so that we can + # use RevertableLists if we want. + _list_type = list + + def __init__(self, *args, **properties): + + self.children = self._list_type() + self.child = None + self.offsets = self._list_type() + + for i in args: + self.add(i) + + super(Container, self).__init__(**properties) + + def set_style_prefix(self, prefix, root): + super(Container, self).set_style_prefix(prefix, root) + + for i in self.children: + i.set_style_prefix(prefix, False) + + def add(self, d): + """ + Adds a child to this container. + """ + + child = renpy.easy.displayable(d) + + self.children.append(child) + + self.child = child + self.offsets = self._list_type() + + def remove(self, d): + """ + Removes the first instance of child from this container. May + not work with all containers. + """ + + for i, c in enumerate(self.children): + if c is d: + break + else: + return + + self.children.pop(i) # W0631 + self.offsets = self._list_type() + + if self.children: + self.child = self.children[-1] + else: + self.child = None + + + def update(self): + """ + This should be called if a child is added to this + displayable outside of the render function. + """ + + renpy.display.render.invalidate(self) + + + def render(self, width, height, st, at): + + rv = Render(width, height) + self.offsets = self._list_type() + + for c in self.children: + cr = render(c, width, height, st, at) + offset = c.place(rv, 0, 0, width, height, cr) + self.offsets.append(offset) + + return rv + + + def event(self, ev, x, y, st): + + children = self.children + offsets = self.offsets + + for i in range(len(offsets) - 1, -1, -1): + + d = children[i] + xo, yo = offsets[i] + + rv = d.event(ev, x - xo, y - yo, st) + if rv is not None: + return rv + + return None + + def visit(self): + return self.children + + # These interact with the ui functions to allow use as a context + # manager. + + def __enter__(self): + + renpy.ui.context_enter(self) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + renpy.ui.context_exit(self) + return False + + + + +def LiveComposite(size, *args, **properties): + """ + :args: disp_imagelike + + This creates a new displayable of `size`, by compositing other + displayables. `size` is a (width, height) tuple. + + The remaining positional arguments are used to place images inside + the LiveComposite. The remaining positional arguments should come + in groups of two, with the first member of each group an (x, y) + tuple, and the second member of a group is a displayable that + is composited at that position. + + Displayables are composited from back to front. + + :: + + image eileen composite = LiveComposite( + (300, 600), + (0, 0), "body.png", + (0, 0), "clothes.png", + (50, 50), "expression.png") + """ + + properties.setdefault('style', 'image_placement') + + width, height = size + + rv = Fixed(xmaximum=width, ymaximum=height, xminimum=width, yminimum=height, **properties) + + if len(args) % 2 != 0: + raise Exception("LiveComposite requires an odd number of arguments.") + + for pos, widget in zip(args[0::2], args[1::2]): + xpos, ypos = pos + rv.add(Position(renpy.easy.displayable(widget), + xpos=xpos, xanchor=0, ypos=ypos, yanchor=0)) + + return rv + +class Position(Container): + """ + Controls the placement of a displayable on the screen, using + supplied position properties. This is the non-curried form of + Position, which should be used when the user has directly created + the displayable that will be shown on the screen. + """ + + def __init__(self, child, style='image_placement', **properties): + """ + @param child: The child that is being laid out. + + @param style: The base style of this position. + + @param properties: Position properties that control where the + child of this widget is placed. + """ + + super(Position, self).__init__(style=style, **properties) + self.add(child) + + def render(self, width, height, st, at): + + surf = render(self.child, width, height, st, at) + + self.offsets = [ (0, 0) ] + + rv = renpy.display.render.Render(surf.width, surf.height) + rv.blit(surf, (0, 0)) + + return rv + + def get_placement(self): + + xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = self.child.get_placement() + + v = self.style.xpos + if v is not None: + xpos = v + + v = self.style.ypos + if v is not None: + ypos = v + + v = self.style.xanchor + if v is not None: + xanchor = v + + v = self.style.yanchor + if v is not None: + yanchor = v + + v = self.style.xoffset + if v is not None: + xoffset = v + + v = self.style.yoffset + if v is not None: + yoffset = v + + v = self.style.subpixel + if v is not None: + subpixel = v + + return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel + + +class Grid(Container): + """ + A grid is a widget that evenly allocates space to its children. + The child widgets should not be greedy, but should instead be + widgets that only use part of the space available to them. + """ + + def __init__(self, cols, rows, padding=None, + transpose=False, + style='grid', **properties): + """ + @param cols: The number of columns in this widget. + + @params rows: The number of rows in this widget. + + @params transpose: True if the grid should be transposed. + """ + + if padding is not None: + properties.setdefault('spacing', padding) + + super(Grid, self).__init__(style=style, **properties) + + cols = int(cols) + rows = int(rows) + + self.cols = cols + self.rows = rows + + self.transpose = transpose + + def render(self, width, height, st, at): + + # For convenience and speed. + padding = self.style.spacing + cols = self.cols + rows = self.rows + + if len(self.children) != cols * rows: + if len(self.children) < cols * rows: + raise Exception("Grid not completely full.") + else: + raise Exception("Grid overfull.") + + # If necessary, transpose the grid (kinda hacky, but it works here.) + if self.transpose: + self.transpose = False + + old_children = self.children[:] + + for y in range(0, rows): + for x in range(0, cols): + self.children[x + y * cols] = old_children[ y + x * rows ] + + + # Now, start the actual rendering. + + renwidth = width + renheight = height + + if self.style.xfill: + renwidth = (width - (cols - 1) * padding) / cols + if self.style.yfill: + renheight = (height - (rows - 1) * padding) / rows + + renders = [ render(i, renwidth, renheight, st, at) for i in self.children ] + sizes = [ i.get_size() for i in renders ] + + cwidth = 0 + cheight = 0 + + for w, h in sizes: + cwidth = max(cwidth, w) + cheight = max(cheight, h) + + if self.style.xfill: + cwidth = renwidth + + if self.style.yfill: + cheight = renheight + + width = cwidth * cols + padding * (cols - 1) + height = cheight * rows + padding * (rows - 1) + + rv = renpy.display.render.Render(width, height) + + self.offsets = [ ] + + for y in range(0, rows): + for x in range(0, cols): + + child = self.children[ x + y * cols ] + surf = renders[x + y * cols] + + xpos = x * (cwidth + padding) + ypos = y * (cheight + padding) + + offset = child.place(rv, xpos, ypos, cwidth, cheight, surf) + self.offsets.append(offset) + + return rv + +class IgnoreLayers(Exception): + """ + Raise this to have the event ignored by layers, but reach the + underlay. + """ + + pass + +class MultiBox(Container): + + layer_name = None + first = True + order_reverse = False + + def __init__(self, spacing=None, layout=None, style='default', **properties): + + if spacing is not None: + properties['spacing'] = spacing + + super(MultiBox, self).__init__(style=style, **properties) + + self.default_layout = layout + + # The start and animation times for children of this + # box. + self.start_times = [ ] + self.anim_times = [ ] + + # A map from layer name to the widget corresponding to + # that layer. + self.layers = None + + # The scene list for this widget. + self.scene_list = None + + def add(self, widget, start_time=None, anim_time=None): # W0221 + super(MultiBox, self).add(widget) + self.start_times.append(start_time) + self.anim_times.append(anim_time) + + def append_scene_list(self, l): + + for sle in l: + self.add(sle.displayable, sle.show_time, sle.animation_time) + + if self.scene_list is None: + self.scene_list = [ ] + + self.scene_list.extend(l) + + def render(self, width, height, st, at): + + # Do we need to adjust the child times due to our being a layer? + if self.layer_name or (self.layers is not None): + adjust_times = True + else: + adjust_times = False + + xminimum = self.style.xminimum + if xminimum is not None: + width = max(width, scale(xminimum, width)) + + yminimum = self.style.yminimum + if yminimum is not None: + height = max(height, scale(yminimum, height)) + + if self.first: + + self.first = False + + if adjust_times: + + it = renpy.game.interface.interact_time + + self.start_times = [ i or it for i in self.start_times ] + self.anim_times = [ i or it for i in self.anim_times ] + + layout = self.style.box_layout + + if layout is None: + layout = self.default_layout + + self.layout = layout # W0201 + + else: + layout = self.layout + + + # Handle time adjustment, store the results in csts and cats. + if adjust_times: + t = renpy.game.interface.frame_time + + csts = [ t - start for start in self.start_times ] + cats = [ t - anim for anim in self.anim_times ] + + else: + csts = [ st ] * len(self.children) + cats = [ at ] * len(self.children) + + offsets = [ ] + + if layout == "fixed": + + rv = None + + if self.style.order_reverse: + iterator = list(zip(reversed(self.children), reversed(csts), reversed(cats))) + else: + iterator = list(zip(self.children, csts, cats)) + + for child, cst, cat in iterator: + + surf = render(child, width, height, cst, cat) + + if rv is None: + + if self.style.fit_first: + sw, sh = surf.get_size() + width = min(width, sw) + height = min(height, sh) + + + rv = renpy.display.render.Render(width, height, layer_name=self.layer_name) + + if surf: + offset = child.place(rv, 0, 0, width, height, surf) + offsets.append(offset) + else: + offsets.append((0, 0)) + + if rv is None: + rv = renpy.display.render.Render(width, height, layer_name=self.layer_name) + + if self.style.order_reverse: + offsets.reverse() + + self.offsets = offsets + + return rv + + # If we're here, we have a box, either horizontal or vertical. Which is good, + # as we can share some code between boxes. + + + spacing = self.style.spacing + first_spacing = self.style.first_spacing + + if first_spacing is None: + first_spacing = spacing + + spacings = [ first_spacing ] + [ spacing ] * (len(self.children) - 1) + + box_wrap = self.style.box_wrap + + xfill = self.style.xfill + yfill = self.style.yfill + + # The shared height and width of the current line. The line_height must + # be 0 for a vertical box, and the line_width must be 0 for a horizontal + # box. + line_width = 0 + line_height = 0 + + # The children to layout. + children = list(self.children) + if self.style.box_reverse: + children.reverse() + spacings.reverse() + + # a list of (child, x, y, w, h, surf) tuples that are turned into + # calls to child.place(). + placements = [ ] + + # The maximum x and y. + maxx = 0 + maxy = 0 + + def layout_line(line, xfill, yfill): + """ + Lays out a single line. + + `line` a list of (child, x, y, surf) tuples. + `xfill` the amount of space to add in the x direction. + `yfill` the amount of space to add in the y direction. + """ + + xfill = max(0, xfill) + yfill = max(0, yfill) + + if line: + xperchild = xfill / len(line) + yperchild = yfill / len(line) + else: + xperchild = 0 + yperchild = 0 + + maxxout = maxx + maxyout = maxy + + for i, (child, x, y, surf) in enumerate(line): + sw, sh = surf.get_size() + sw = max(line_width, sw) + sh = max(line_height, sh) + + x += i * xperchild + y += i * yperchild + + sw += xperchild + sh += yperchild + + placements.append((child, x, y, sw, sh, surf)) + + maxxout = max(maxxout, x + sw) + maxyout = max(maxyout, y + sh) + + return maxxout, maxyout + + x = 0 + y = 0 + + full_width = False + full_height = False + + if layout == "horizontal": + + full_height = yfill + + line_height = 0 + line = [ ] + remwidth = width + + for d, padding, cst, cat in zip(children, spacings, csts, cats): + + if box_wrap: + rw = width + else: + rw = remwidth + + surf = render(d, rw, height - y, cst, cat) + sw, sh = surf.get_size() + + if box_wrap and remwidth - sw - padding <= 0 and line: + maxx, maxy = layout_line(line, remwidth if xfill else 0, 0) + + y += line_height + x = 0 + line_height = 0 + remwidth = width + line = [ ] + + + line.append((d, x, y, surf)) + line_height = max(line_height, sh) + x += sw + padding + remwidth -= (sw + padding) + + maxx, maxy = layout_line(line, remwidth if xfill else 0, 0) + + + elif layout == "vertical": + + full_width = xfill + + line_width = 0 + line = [ ] + remheight = height + + for d, padding, cst, cat in zip(children, spacings, csts, cats): + + if box_wrap: + rh = height + else: + rh = remheight + + surf = render(d, width - x, rh, cst, cat) + sw, sh = surf.get_size() + + if box_wrap and remheight - sh - padding <= 0: + maxx, maxy = layout_line(line, 0, remheight if yfill else 0) + + x += line_width + y = 0 + line_width = 0 + remheight = height + line = [ ] + + line.append((d, x, y, surf)) + line_width = max(line_width, sw) + y += sh + padding + remheight -= (sh + padding) + + maxx, maxy = layout_line(line, 0, remheight if yfill else 0) + + # Back to the common for vertical and horizontal. + + if not xfill: + width = maxx + + if not yfill: + height = maxy + + rv = renpy.display.render.Render(width, height) + + if self.style.box_reverse ^ self.style.order_reverse: + placements.reverse() + + for child, x, y, w, h, surf in placements: + if full_width: + w = width + if full_height: + h = height + + offset = child.place(rv, x, y, w, h, surf) + offsets.append(offset) + + if self.style.order_reverse: + offsets.reverse() + + self.offsets = offsets + + return rv + + + def event(self, ev, x, y, st): + + + children_offsets = list(zip(self.children, self.offsets, self.start_times)) + + if not self.style.order_reverse: + children_offsets.reverse() + + try: + + for i, (xo, yo), t in children_offsets: + + if t is None: + cst = st + else: + cst = renpy.game.interface.event_time - t + + rv = i.event(ev, x - xo, y - yo, cst) + if rv is not None: + return rv + + except IgnoreLayers: + if self.layers: + return None + else: + raise + + return None + +def Fixed(**properties): + return MultiBox(layout='fixed', **properties) + +class SizeGroup(renpy.object.Object): + + def __init__(self): + + super(SizeGroup, self).__init__() + + self.members = [ ] + self._width = None + self.computing_width = False + + def width(self, width, height, st, at): + if self._width is not None: + return self._width + + if self.computing_width: + return 0 + + self.computing_width = True + + maxwidth = 0 + + for i in self.members: + rend = renpy.display.render.render(i, width, height, st, at) + maxwidth = max(rend.width, maxwidth) + renpy.display.render.invalidate(i) + + self._width = maxwidth + self.computing_width = False + + return maxwidth + + +size_groups = dict() + +class Window(Container): + """ + A window that has padding and margins, and can place a background + behind its child. `child` is the child added to this + displayable. All other properties are as for the :ref:`Window` + screen language statement. + """ + + def __init__(self, child, style='window', **properties): + + super(Window, self).__init__(style=style, **properties) + if child is not None: + self.add(child) + + def visit(self): + return [ self.style.background ] + self.children + + def get_child(self): + return self.style.child or self.child + + def per_interact(self): + size_group = self.style.size_group + + if size_group: + group = size_groups.get(size_group, None) + if group is None: + group = size_groups[size_group] = SizeGroup() + + group.members.append(self) + + def predict_one(self): + # Child will be predicted by visiting. + + pd = renpy.display.predict.displayable + style = self.style + + pd(style.insensitive_background) + pd(style.idle_background) + pd(style.hover_background) + pd(style.selected_idle_background) + pd(style.selected_hover_background) + + pd(style.insensitive_child) + pd(style.idle_child) + pd(style.hover_child) + pd(style.selected_idle_child) + pd(style.selected_hover_child) + + pd(style.insensitive_foreground) + pd(style.idle_foreground) + pd(style.hover_foreground) + pd(style.selected_idle_foreground) + pd(style.selected_hover_foreground) + + def render(self, width, height, st, at): + + # save some typing. + style = self.style + + xminimum = scale(style.xminimum, width) + yminimum = scale(style.yminimum, height) + + size_group = self.style.size_group + if size_group and size_group in size_groups: + xminimum = max(xminimum, size_groups[size_group].width(width, height, st, at)) + + left_margin = scale(style.left_margin, width) + left_padding = scale(style.left_padding, width) + + right_margin = scale(style.right_margin, width) + right_padding = scale(style.right_padding, width) + + top_margin = scale(style.top_margin, height) + top_padding = scale(style.top_padding, height) + + bottom_margin = scale(style.bottom_margin, height) + bottom_padding = scale(style.bottom_padding, height) + + # c for combined. + cxmargin = left_margin + right_margin + cymargin = top_margin + bottom_margin + + cxpadding = left_padding + right_padding + cypadding = top_padding + bottom_padding + + child = self.get_child() + + # Render the child. + surf = render(child, + width - cxmargin - cxpadding, + height - cymargin - cypadding, + st, at) + + sw, sh = surf.get_size() + + # If we don't fill, shrink our size to fit. + + if not style.xfill: + width = max(cxmargin + cxpadding + sw, xminimum) + + if not style.yfill: + height = max(cymargin + cypadding + sh, yminimum) + + rv = renpy.display.render.Render(width, height) + + # Draw the background. The background should render at exactly the + # requested size. (That is, be a Frame or a Solid). + if style.background: + bw = width - cxmargin + bh = height - cymargin + + back = render(style.background, bw, bh, st, at) + + style.background.place(rv, left_margin, top_margin, bw, bh, back, main=False) + + offsets = child.place(rv, + left_margin + left_padding, + top_margin + top_padding, + width - cxmargin - cxpadding, + height - cymargin - cypadding, + surf) + + # Draw the foreground. The background should render at exactly the + # requested size. (That is, be a Frame or a Solid). + if style.foreground: + bw = width - cxmargin + bh = height - cymargin + + back = render(style.foreground, bw, bh, st, at) + + style.foreground.place(rv, left_margin, top_margin, bw, bh, back, main=False) + + self.offsets = [ offsets ] + + self.window_size = width, height # W0201 + + return rv + + +def dynamic_displayable_compat(st, at, expr): + child = renpy.python.py_eval(expr) + return child, None + +class DynamicDisplayable(renpy.display.core.Displayable): + """ + :doc: disp_dynamic + + A displayable that can change its child based on a Python + function, over the course of an interaction. + + `function` + A function that is called with the arguments: + + * The amount of time the displayable has been shown for. + * The amount of time any displayable with the same tag has been shown for. + * Any positional or keyword arguments supplied to DynamicDisplayable. + + and should return a (d, redraw) tuple, where: + + * `d` is a displayable to show. + * `redraw` is the amount of time to wait before calling the + function again, or None to not call the function again + before the start of the next interaction. + + `function` is called at the start of every interaction. + + As a special case, `function` may also be a python string that evaluates + to a displayable. In that case, function is run once per interaction. + + :: + + # If tooltip is not empty, shows it in a text. Otherwise, + # show Null. Checks every tenth of a second to see if the + # tooltip has been updated. + init python: + def show_tooltip(st, at): + if tooltip: + return tooltip, .1 + else: + return Null() + + image tooltipper = DynamicDisplayable(show_tooltip) + + """ + + nosave = [ 'child' ] + + def after_setstate(self): + self.child = None + + def __init__(self, function, *args, **kwargs): + + super(DynamicDisplayable, self).__init__() + self.child = None + + if isinstance(function, str): + args = ( function, ) + kwargs = { } + function = dynamic_displayable_compat + + self.predict_function = kwargs.pop("_predict_function", None) + self.function = function + self.args = args + self.kwargs = kwargs + self.st = 0 + self.at = 0 + + def visit(self): + return [ ] + + def per_interact(self): + child, _ = self.function(self.st, self.at, *self.args, **self.kwargs) + child = renpy.easy.displayable(child) + child.visit_all(lambda a : a.per_interact()) + + if child is not self.child: + renpy.display.render.redraw(self, 0) + self.child = child + + def render(self, w, h, st, at): + + self.st = st + self.at = at + + child, redraw = self.function(st, at, *self.args, **self.kwargs) + child = renpy.easy.displayable(child) + child.visit_all(lambda c : c.per_interact()) + + self.child = child + + if redraw is not None: + renpy.display.render.redraw(self, redraw) + + return renpy.display.render.render(self.child, w, h, st, at) + + def predict_one(self): + if not self.predict_function: + return + + for i in self.predict_function(*self.args, **self.kwargs): + if i is not None: + renpy.display.predict.displayable(i) + + def get_placement(self): + if not self.child: + self.per_interact() + + return self.child.get_placement() + + + def event(self, ev, x, y, st): + if self.child: + return self.child.event(ev, x, y, st) + +# This chooses the first member of switch that's being shown on the +# given layer. +def condition_switch_pick(switch): + for cond, d in switch: + if cond is None or renpy.python.py_eval(cond): + return d + + raise Exception("Switch could not choose a displayable.") + +def condition_switch_show(st, at, switch): + return condition_switch_pick(switch), None + +def condition_switch_predict(switch): + + if renpy.game.lint: + return [ d for _cond, d in switch ] + + return [ condition_switch_pick(switch) ] + +def ConditionSwitch(*args, **kwargs): + """ + :doc: disp_dynamic + + This is a displayable that changes what it is showing based on + python conditions. The positional argument should be given in + groups of two, where each group consists of: + + * A string containing a python condition. + * A displayable to use if the condition is true. + + The first true condition has its displayable shown, at least + one condition should always be true. + + :: + + image jill = ConditionSwitch( + "jill_beers > 4", "jill_drunk.png", + "True", "jill_sober.png") + """ + + kwargs.setdefault('style', 'default') + + switch = [ ] + + if len(args) % 2 != 0: + raise Exception('ConditionSwitch takes an even number of arguments') + + for cond, d in zip(args[0::2], args[1::2]): + + d = renpy.easy.displayable(d) + switch.append((cond, d)) + + rv = DynamicDisplayable(condition_switch_show, + switch, + _predict_function=condition_switch_predict) + + return Position(rv, **kwargs) + + +def ShowingSwitch(*args, **kwargs): + """ + :doc: disp_dynamic + + This is a displayable that changes what it is showing based on the + images are showing on the screen. The positional argument should + be given in groups of two, where each group consists of: + + * A string giving an image name, or None to indicate the default. + * A displayable to use if the condition is true. + + A default image should be specified. + + One use of ShowingSwitch is to have side images change depending on + the current emotion of a character. For example:: + + define e = Character("Eileen", + show_side_image=ShowingSwitch( + "eileen happy", Image("eileen_happy_side.png", xalign=1.0, yalign=1.0), + "eileen vhappy", Image("eileen_vhappy_side.png", xalign=1.0, yalign=1.0), + None, Image("eileen_happy_default.png", xalign=1.0, yalign=1.0), + ) + ) + """ + + layer = kwargs.pop('layer', 'master') + + if len(args) % 2 != 0: + raise Exception('ShowingSwitch takes an even number of positional arguments') + + condargs = [ ] + + + for name, d in zip(args[0::2], args[1::2]): + if name is not None: + if not isinstance(name, tuple): + name = tuple(name.split()) + cond = "renpy.showing(%r, layer=%r)" % (name, layer) + else: + cond = None + + + condargs.append(cond) + condargs.append(d) + + return ConditionSwitch(*condargs, **kwargs) + + +class IgnoresEvents(Container): + + def __init__(self, child, **properties): + super(IgnoresEvents, self).__init__(**properties) + self.add(child) + + def render(self, w, h, st, at): + cr = renpy.display.render.render(self.child, w, h, st, at) + cw, ch = cr.get_size() + rv = renpy.display.render.Render(cw, ch) + rv.blit(cr, (0, 0), focus=False) + + return rv + + def get_placement(self): + return self.child.get_placement() + + # Ignores events. + def event(self, ev, x, y, st): + return None + +def edgescroll_proportional(n): + """ + An edgescroll function that causes the move speed to be proportional + from the edge distance. + """ + return n + +class Viewport(Container): + + __version__ = 3 + + def after_upgrade(self, version): + if version < 1: + self.xadjustment = renpy.display.behavior.Adjustment(1, 0) + self.yadjustment = renpy.display.behavior.Adjustment(1, 0) + self.set_adjustments = False + self.mousewheel = False + self.draggable = False + self.width = 0 + self.height = 0 + + if version < 2: + self.drag_position = None + + if version < 3: + self.edge_size = False + self.edge_speed = False + self.edge_function = None + self.edge_xspeed = 0 + self.edge_yspeed = 0 + self.edge_last_st = None + + def __init__(self, + child=None, + child_size=(None, None), + offsets=(None, None), + xadjustment=None, + yadjustment=None, + set_adjustments=True, + mousewheel=False, + draggable=False, + edgescroll=None, + style='viewport', + xinitial=None, + yinitial=None, + replaces=None, + **properties): + + super(Viewport, self).__init__(style=style, **properties) + if child is not None: + self.add(child) + + if xadjustment is None: + self.xadjustment = renpy.display.behavior.Adjustment(1, 0) + else: + self.xadjustment = xadjustment + + if yadjustment is None: + self.yadjustment = renpy.display.behavior.Adjustment(1, 0) + else: + self.yadjustment = yadjustment + + + if isinstance(replaces, Viewport): + self.xadjustment.range = replaces.xadjustment.range + self.yadjustment.range = replaces.yadjustment.range + self.xadjustment.value = replaces.xadjustment.value + self.yadjustment.value = replaces.yadjustment.value + self.xoffset = replaces.xoffset + self.yoffset = replaces.yoffset + self.drag_position = replaces.drag_position + else: + self.xoffset = offsets[0] if (offsets[0] is not None) else xinitial + self.yoffset = offsets[1] if (offsets[1] is not None) else yinitial + self.drag_position = None + + if self.xadjustment.adjustable is None: + self.xadjustment.adjustable = True + + if self.yadjustment.adjustable is None: + self.yadjustment.adjustable = True + + self.set_adjustments = set_adjustments + + self.child_width, self.child_height = child_size + + self.mousewheel = mousewheel + self.draggable = draggable + + self.width = 0 + self.height = 0 + + # The speed at which we scroll in the x and y directions, in pixels + # per second. + self.edge_xspeed = 0 + self.edge_yspeed = 0 + + # The last time we edgescrolled. + self.edge_last_st = None + + if edgescroll is not None: + + # The size of the edges that trigger scrolling. + self.edge_size = edgescroll[0] + + # How far from the edge we can scroll. + self.edge_speed = edgescroll[1] + + if len(edgescroll) >= 3: + self.edge_function = edgescroll[2] + else: + self.edge_function = edgescroll_proportional + + else: + self.edge_size = 0 + self.edge_speed = 0 + self.edge_function = edgescroll_proportional + + + def per_interact(self): + self.xadjustment.register(self) + self.yadjustment.register(self) + + def render(self, width, height, st, at): + + self.width = width + self.height = height + + child_width = self.child_width or width + child_height = self.child_height or height + + surf = render(self.child, child_width, child_height, st, at) + + cw, ch = surf.get_size() + + # width = min(cw, width) + # height = min(ch, height) + + if self.set_adjustments: + self.xadjustment.range = max(cw - width, 0) + self.xadjustment.page = width + self.yadjustment.range = max(ch - height, 0) + self.yadjustment.page = height + + if self.xoffset is not None: + if isinstance(self.xoffset, int): + value = self.xoffset + else: + value = max(cw - width, 0) * self.xoffset + + self.xadjustment.value = value + + if self.yoffset is not None: + if isinstance(self.yoffset, int): + value = self.yoffset + else: + value = max(ch - height, 0) * self.yoffset + + self.yadjustment.value = value + + if self.edge_size and self.edge_last_st and (self.edge_xspeed or self.edge_yspeed): + + duration = max(st - self.edge_last_st, 0) + self.xadjustment.change(self.xadjustment.value + duration * self.edge_xspeed) + self.yadjustment.change(self.yadjustment.value + duration * self.edge_yspeed) + + self.check_edge_redraw() + + self.edge_last_st = st + + cxo = -int(self.xadjustment.value) + cyo = -int(self.yadjustment.value) + + self.offsets = [ (cxo, cyo) ] + + rv = renpy.display.render.Render(width, height) + rv.blit(surf, (cxo, cyo)) + + return rv + + def check_edge_redraw(self): + redraw = False + + if (self.edge_xspeed > 0) and (self.xadjustment.value < self.xadjustment.range): + redraw = True + if (self.edge_xspeed < 0) and (self.xadjustment.value > 0): + redraw = True + + if (self.edge_yspeed > 0) and (self.yadjustment.value < self.yadjustment.range): + redraw = True + if (self.edge_yspeed < 0) and (self.yadjustment.value > 0): + redraw = True + + if redraw: + renpy.display.render.redraw(self, 0) + + + def event(self, ev, x, y, st): + + self.xoffset = None + self.yoffset = None + + rv = super(Viewport, self).event(ev, x, y, st) + if rv is not None: + return rv + + if self.draggable and renpy.display.focus.get_grab() == self: + + oldx, oldy = self.drag_position + dx = x - oldx + dy = y - oldy + + self.xadjustment.change(self.xadjustment.value - dx) + self.yadjustment.change(self.yadjustment.value - dy) + + self.drag_position = (x, y) # W0201 + + if renpy.display.behavior.map_event(ev, 'viewport_drag_end'): + renpy.display.focus.set_grab(None) + raise renpy.display.core.IgnoreEvent() + + if not ((0 <= x < self.width) and (0 <= y <= self.height)): + return + + if self.mousewheel: + + if renpy.display.behavior.map_event(ev, 'viewport_up'): + rv = self.yadjustment.change(self.yadjustment.value - self.yadjustment.step) + if rv is not None: + return rv + else: + raise renpy.display.core.IgnoreEvent() + + if renpy.display.behavior.map_event(ev, 'viewport_down'): + rv = self.yadjustment.change(self.yadjustment.value + self.yadjustment.step) + if rv is not None: + return rv + else: + raise renpy.display.core.IgnoreEvent() + + if self.draggable: + + if renpy.display.behavior.map_event(ev, 'viewport_drag_start'): + self.drag_position = (x, y) + renpy.display.focus.set_grab(self) + raise renpy.display.core.IgnoreEvent() + + if self.edge_size: + + def speed(n, zero, one): + """ + Given a position `n`, computes the speed. The speed is 0.0 + when `n` == `zero`, 1.0 when `n` == `one`, and linearly + interpolated when between. + + Returns 0.0 when outside the bounds - in either direction. + """ + + n = 1.0 * (n - zero) / (one - zero) + if n < 0.0: + return 0.0 + if n > 1.0: + return 0.0 + + return n + + xspeed = speed(x, self.width - self.edge_size, self.width) + xspeed -= speed(x, self.edge_size, 0) + self.edge_xspeed = self.edge_speed * self.edge_function(xspeed) + + yspeed = speed(y, self.height - self.edge_size, self.height) + yspeed -= speed(y, self.edge_size, 0) + self.edge_yspeed = self.edge_speed * self.edge_function(yspeed) + + if xspeed or yspeed: + self.check_edge_redraw() + if self.edge_last_st is None: + self.edge_last_st = st + else: + self.edge_last_st = None + + return None + + def set_xoffset(self, offset): + self.xoffset = offset + renpy.display.render.redraw(self, 0) + + def set_yoffset(self, offset): + self.yoffset = offset + renpy.display.render.redraw(self, 0) + +def LiveCrop(rect, child, **properties): + """ + :doc: disp_imagelike + + This created a displayable by cropping `child` to `rect`, where + `rect` is an (x, y, width, height) tuple. :: + + image eileen cropped = LiveCrop((0, 0, 300, 300), "eileen happy") + """ + + x, y, w, h = rect + + return Viewport(child, offsets=(x, y), xmaximum=w, ymaximum=h, **properties) + +class Side(Container): + + possible_positions = set([ 'tl', 't', 'tr', 'r', 'br', 'b', 'bl', 'l', 'c']) + + def after_setstate(self): + self.sized = False + + def __init__(self, positions, style='side', **properties): + + super(Side, self).__init__(style=style, **properties) + + if isinstance(positions, str): + positions = positions.split() + + for i in positions: + if not i in Side.possible_positions: + raise Exception("Side used with impossible position '%s'." % (i,)) + + self.positions = tuple(positions) + self.sized = False + + def render(self, width, height, st, at): + + pos_d = { } + pos_i = { } + + for i, (pos, d) in enumerate(zip(self.positions, self.children)): + pos_d[pos] = d + pos_i[pos] = i + + # Figure out the size of each widget (and hence where the + # widget needs to be placed). + + if not self.sized: + self.sized = True + + # Deal with various spacings. + spacing = self.style.spacing + + def spacer(a, b, c, axis): + if (a in pos_d) or (b in pos_d) or (c in pos_d): + return spacing, axis - spacing + else: + return 0, axis + + self.left_space, width = spacer('tl', 'l', 'bl', width) # W0201 + self.right_space, width = spacer('tr', 'r', 'br', width) # W0201 + self.top_space, height = spacer('tl', 't', 'tr', height) # W0201 + self.bottom_space, height = spacer('bl', 'b', 'br', height) # W0201 + + # The sizes of the various borders. + left = 0 + right = 0 + top = 0 + bottom = 0 + cwidth = 0 + cheight = 0 + + def sizeit(pos, width, height, owidth, oheight): + if pos not in pos_d: + return owidth, oheight + + rend = render(pos_d[pos], width, height, st, at) + rv = max(owidth, rend.width), max(oheight, rend.height) + rend.kill() + return rv + + cwidth, cheight = sizeit('c', width, height, 0, 0) + cwidth, top = sizeit('t', cwidth, height, cwidth, top) + cwidth, bottom = sizeit('b', cwidth, height, cwidth, bottom) + left, cheight = sizeit('l', width, cheight, left, cheight) + right, cheight = sizeit('r', width, cheight, right, cheight) + + left, top = sizeit('tl', left, top, left, top) + left, bottom = sizeit('bl', left, bottom, left, bottom) + right, top = sizeit('tr', right, top, right, top) + right, bottom = sizeit('br', right, bottom, right, bottom) + + self.cwidth = cwidth # W0201 + self.cheight = cheight # W0201 + + self.top = top # W0201 + self.bottom = bottom # W0201 + self.left = left # W0201 + self.right = right # W0201 + + else: + cwidth = self.cwidth + cheight = self.cheight + top = self.top + bottom = self.bottom + left = self.left + right = self.right + + # Now, place everything onto the render. + + self.offsets = [ None ] * len(self.children) + + lefts = self.left_space + rights = self.right_space + tops = self.top_space + bottoms = self.bottom_space + + + cwidth = min(cwidth, width - left - lefts - right - rights) + cheight = min(cheight, height - top - tops - bottom - bottoms) + + rv = renpy.display.render.Render(left + lefts + cwidth + rights + right, + top + tops + cheight + bottoms + bottom) + + def place(pos, x, y, w, h): + + if pos not in pos_d: + return + + d = pos_d[pos] + i = pos_i[pos] + rend = render(d, w, h, st, at) + self.offsets[i] = pos_d[pos].place(rv, x, y, w, h, rend) + + col1 = 0 + col2 = left + lefts + col3 = left + lefts + cwidth + rights + + row1 = 0 + row2 = top + tops + row3 = top + tops + cheight + bottoms + + place('c', col2, row2, cwidth, cheight) + + place('t', col2, row1, cwidth, top) + place('r', col3, row2, right, cheight) + place('b', col2, row3, cwidth, bottom) + place('l', col1, row2, left, cheight) + + place('tl', col1, row1, left, top) + place('tr', col3, row1, right, top) + place('br', col3, row3, right, bottom) + place('bl', col1, row3, left, bottom) + + return rv + +class Alpha(renpy.display.core.Displayable): + def __init__(self, start, end, time, child=None, repeat=False, bounce=False, + anim_timebase=False, time_warp=None, **properties): + + super(Alpha, self).__init__(**properties) + + self.start = start + self.end = end + self.time = time + self.child = renpy.easy.displayable(child) + self.repeat = repeat + self.anim_timebase = anim_timebase + self.time_warp = time_warp + + def visit(self): + return [ self.child ] + + def render(self, height, width, st, at): + if self.anim_timebase: + t = at + else: + t = st + + if self.time: + done = min(t / self.time, 1.0) + else: + done = 1.0 + + if renpy.game.less_updates: + done = 1.0 + elif self.repeat: + done = done % 1.0 + renpy.display.render.redraw(self, 0) + elif done != 1.0: + renpy.display.render.redraw(self, 0) + + if self.time_warp: + done = self.time_warp(done) + + alpha = self.start + done * (self.end - self.start) + + rend = renpy.display.render.render(self.child, height, width, st, at) + + w, h = rend.get_size() + rv = renpy.display.render.Render(w, h) + rv.blit(rend, (0, 0)) + rv.alpha = alpha + + return rv + + +class AdjustTimes(Container): + + def __init__(self, child, start_time, anim_time, **properties): + super(AdjustTimes, self).__init__(**properties) + + self.start_time = start_time + self.anim_time = anim_time + + self.add(child) + + def render(self, w, h, st, at): + + if self.start_time is None: + self.start_time = renpy.game.interface.frame_time + + if self.anim_time is None: + self.anim_time = renpy.game.interface.frame_time + + st = renpy.game.interface.frame_time - self.start_time + at = renpy.game.interface.frame_time - self.anim_time + + cr = renpy.display.render.render(self.child, w, h, st, at) + cw, ch = cr.get_size() + rv = renpy.display.render.Render(cw, ch) + rv.blit(cr, (0, 0)) + + self.offsets = [ (0, 0) ] + + return rv + + def get_placement(self): + return self.child.get_placement() + + +class LiveTile(Container): + """ + :doc: disp_imagelike + + Tiles `child` until it fills the area allocated to this displayable. + + :: + + image bg tile = LiveTile("bg.png") + + """ + + def __init__(self, child, style='tile', **properties): + super(LiveTile, self).__init__(style=style, **properties) + + self.add(child) + + def render(self, width, height, st, at): + + cr = renpy.display.render.render(self.child, width, height, st, at) + cw, ch = cr.get_size() + rv = renpy.display.render.Render(width, height) + + width = int(width) + height = int(height) + cw = int(cw) + ch = int(ch) + + for y in range(0, height, ch): + for x in range(0, width, cw): + rv.blit(cr, (x, y), focus=False) + + return rv |