# Copyright 2004-2013 Tom Rothamel # # 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