# Copyright 2004-2019 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 __future__ import print_function 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 :name: Null 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 _handles_event(self, event): for i in self.children: if i._handles_event(event): return True return False 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 _duplicate(self, args): if args and args.args: args.extraneous() if not self._duplicatable: return self rv = self._copy(args) rv.children = [ i._duplicate(args) for i in self.children ] if rv.children: rv.child = rv.children[-1] rv._duplicatable = False for i in rv.children: i._unique() if i._duplicatable: rv._duplicatable = True return rv def _in_current_store(self): children = [ ] changed = False for old in self.children: new = old._in_current_store() changed |= (old is not new) children.append(new) if not changed: return self rv = self._copy() rv.children = children if rv.children: rv.child = rv.children[-1] return rv 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() if child._duplicatable: self._duplicatable = True def _clear(self): self.child = None self.children = self._list_type() self.offsets = self._list_type() renpy.display.render.redraw(self, 0) 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 # In #641, these went out of sync. Since they should resync on a # render, ignore the event for a short while rather than crashing. if len(offsets) != len(children): return None for i in xrange(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 Composite(size, *args, **properties): """ :name: Composite :doc: 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 = Composite( (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(widget, xpos=xpos, xanchor=0, ypos=ypos, yanchor=0)) return rv LiveComposite = Composite class Position(Container): """ :undocumented: 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() if xoffset is None: xoffset = 0 if yoffset is None: yoffset = 0 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 (not subpixel) and (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): xspacing = self.style.xspacing yspacing = self.style.yspacing if xspacing is None: xspacing = self.style.spacing if yspacing is None: yspacing = self.style.spacing # For convenience and speed. 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 self.transpose: children = [ ] for y in range(rows): for x in range(cols): children.append(self.children[y + x * rows]) else: children = self.children # Now, start the actual rendering. renwidth = width renheight = height if self.style.xfill: renwidth = (width - (cols - 1) * xspacing) / cols if self.style.yfill: renheight = (height - (rows - 1) * yspacing) / rows renders = [ render(i, renwidth, renheight, st, at) for i in 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 + xspacing * (cols - 1) height = cheight * rows + yspacing * (rows - 1) rv = renpy.display.render.Render(width, height) offsets = [ ] for y in range(0, rows): for x in range(0, cols): child = children[ x + y * cols ] surf = renders[x + y * cols] xpos = x * (cwidth + xspacing) ypos = y * (cheight + yspacing) offset = child.place(rv, xpos, ypos, cwidth, cheight, surf) offsets.append(offset) if self.transpose: self.offsets = [ ] for x in range(cols): for y in range(rows): self.offsets.append(offsets[y * cols + x]) else: self.offsets = offsets 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 layout = None 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._clipping = self.style.clipping 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 _clear(self): super(MultiBox, self)._clear() self.start_times = [ ] self.anim_times = [ ] self.layers = None self.scene_list = None def _in_current_store(self): if self.layer_name is not None: if self.scene_list is None: return self scene_list = [ ] changed = False for old_sle in self.scene_list: new_sle = old_sle.copy() d = new_sle.displayable._in_current_store() if d is not new_sle.displayable: new_sle.displayable = d changed = True scene_list.append(new_sle) if not changed: return self rv = MultiBox(layout=self.default_layout) rv.layer_name = self.layer_name rv.append_scene_list(scene_list) elif self.layers: rv = MultiBox(layout=self.default_layout) rv.layers = { } changed = False for layer in renpy.config.layers: old_d = self.layers[layer] new_d = old_d._in_current_store() if new_d is not old_d: changed = True rv.add(new_d) rv.layers[layer] = new_d if not changed: return self else: return super(MultiBox, self)._in_current_store() if self.offsets: rv.offsets = list(self.offsets) if self.start_times: rv.start_times = list(self.start_times) if self.anim_times: rv.anim_times = list(self.anim_times) return rv def __unicode__(self): layout = self.style.box_layout if layout is None: layout = self.default_layout if layout == "fixed": return "Fixed" elif layout == "horizontal": return "HBox" elif layout == "vertical": return "VBox" else: return "MultiBox" 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 update_times(self): 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 ] if it is None: self.first = True 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 minx = self.style.xminimum if minx is not None: width = max(width, scale(minx, width)) miny = self.style.yminimum if miny is not None: height = max(height, scale(miny, height)) if self.first: self.first = False if adjust_times: self.update_times() layout = self.style.box_layout if layout is None: layout = self.default_layout # Handle time adjustment, store the results in csts and cats. if adjust_times: t = renpy.game.interface.frame_time csts = [ 0 if (start is None) else (t - start) for start in self.start_times ] cats = [ 0 if (anim is None) else (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 = zip(reversed(self.children), reversed(csts), reversed(cats)) else: iterator = zip(self.children, csts, cats) rv = renpy.display.render.Render(width, height, layer_name=self.layer_name) xfit = self.style.xfit yfit = self.style.yfit fit_first = self.style.fit_first if fit_first == "width": first_fit_width = True first_fit_height = False elif fit_first == "height": first_fit_width = False first_fit_height = True elif fit_first: first_fit_width = True first_fit_height = True else: first_fit_width = False first_fit_height = False sizes = [ ] for child, cst, cat in iterator: surf = render(child, width, height, cst, cat) size = surf.get_size() sizes.append(size) if first_fit_width: width = rv.width = size[0] first_fit_width = False if first_fit_height: height = rv.height = size[1] first_fit_height = False if surf: offset = child.place(rv, 0, 0, width, height, surf) offsets.append(offset) else: offsets.append((0, 0)) if xfit: width = 0 for o, s in zip(offsets, sizes): width = max(o[0] + s[0], width) if fit_first: break rv.width = width if width > renpy.config.max_fit_size: raise Exception("Fixed fit width ({}) is too large.".format(width)) if yfit: height = 0 for o, s in zip(offsets, sizes): height = max(o[1] + s[1], height) if fit_first: break rv.height = height if height > renpy.config.max_fit_size: raise Exception("Fixed fit width ({}) is too large.".format(height)) 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 box_wrap_spacing = self.style.box_wrap_spacing xfill = self.style.xfill yfill = self.style.yfill xminimum = self.style.xminimum yminimum = self.style.yminimum # 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 # The minimum size of x and y. minx = 0 miny = 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 if layout == "horizontal": if yfill: miny = height else: miny = yminimum line_height = 0 line = [ ] remwidth = width if xfill: target_width = width else: target_width = xminimum 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, target_width - x, 0) y += line_height + box_wrap_spacing 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, target_width - x, 0) elif layout == "vertical": if xfill: minx = width else: minx = xminimum line_width = 0 line = [ ] remheight = height if yfill: target_height = height else: target_height = yminimum 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, target_height - y) x += line_width + box_wrap_spacing 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, target_height - y) else: raise Exception("Unknown box layout: %r" % layout) # Back to the common for vertical and horizontal. if not xfill: width = max(xminimum, maxx) if not yfill: height = max(yminimum, 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: w = max(minx, w) h = max(miny, h) 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): # Do we need to adjust the child times due to our being a layer? if self.first: self.first = False if self.layer_name or (self.layers is not None): self.update_times() children_offsets = 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: if ev.type != renpy.display.core.TIMEEVENT: renpy.display.interface.post_time_event() 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_for_size(i, width, height, st, at) maxwidth = max(rend.width, maxwidth) 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=None, style='window', **properties): super(Window, self).__init__(style=style, **properties) if child is not None: self.add(child) def visit(self): rv = [ ] self.style._visit_window(rv.append) return rv + 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 render(self, width, height, st, at): # save some typing. style = self.style xminimum = scale(style.xminimum, width) yminimum = scale(style.yminimum, height) xmaximum = scale(style.xmaximum, width) ymaximum = scale(style.ymaximum, 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)) width = max(xminimum, width) height = max(yminimum, height) 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) if renpy.config.enforce_window_max_size: if xmaximum is not None: width = min(width, xmaximum) if ymaximum is not None: height = min(height, ymaximum) 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) if self.child: 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. :: # Shows a countdown from 5 to 0, updating it every tenth of # a second until the time expires. init python: def show_countdown(st, at): if st > 5.0: return Text("0.0"), None else: d = Text("{:.1f}".format(5.0 - st)) return d, 0.1 image countdown = DynamicDisplayable(show_countdown) """ nosave = [ 'child' ] _duplicatable = True def after_setstate(self): self.child = None def __init__(self, function, *args, **kwargs): super(DynamicDisplayable, self).__init__() self.child = None if isinstance(function, basestring): args = ( function, ) kwargs = { } function = dynamic_displayable_compat self.predict_function = kwargs.pop("_predict_function", None) self.function = function self.args = args self.kwargs = kwargs def _duplicate(self, args): rv = self._copy(args) if rv.child is not None and rv.child._duplicateable: rv.child = rv.child._duplicate(args) return rv def visit(self): return [ ] def update(self, st, at): child, redraw = self.function(st, at, *self.args, **self.kwargs) child = renpy.easy.displayable(child) if child._duplicatable: child = child._duplicate(self._args) child._unique() child.visit_all(lambda c : c.per_interact()) self.child = child if redraw is not None: renpy.display.render.redraw(self, redraw) def per_interact(self): renpy.display.render.redraw(self, 0) def render(self, w, h, st, at): self.update(st, at) return renpy.display.render.render(self.child, w, h, st, at) def predict_one(self): try: if self.predict_function: child = self.predict_function(*self.args, **self.kwargs) else: child, _ = self.function(0, 0, *self.args, **self.kwargs) if isinstance(child, list): for i in child: renpy.display.predict.displayable(i) else: renpy.display.predict.displayable(child) except: pass def get_placement(self): if not self.child: self.update(0, 0) return self.child.get_placement() def event(self, ev, x, y, st): if self.child: return self.child.event(ev, x, y, st) # A cache of compiled conditions used by ConditionSwitch. cond_cache = { } # 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: return d if cond in cond_cache: code = cond_cache[cond] else: code = renpy.python.py_compile(cond, 'eval') cond_cache[cond] = code if renpy.python.py_eval_bytecode(code): return d raise Exception("Switch could not choose a displayable.") def condition_switch_show(st, at, switch, predict_all=None): return condition_switch_pick(switch), None def condition_switch_predict(switch, predict_all=None): if predict_all is None: predict_all = renpy.config.conditionswitch_predict_all if renpy.game.lint or (predict_all and renpy.display.predict.predicting): return [ d for _cond, d in switch ] return [ condition_switch_pick(switch) ] def ConditionSwitch(*args, **kwargs): """ :name: ConditionSwitch :doc: disp_dynamic :args: (*args, predict_all=None, **properties) This is a displayable that changes what it is showing based on Python conditions. The positional arguments 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. The conditions uses here should not have externally-visible side-effects. `predict_all` If True, all of the possible displayables will be predicted when the displayable is shown. If False, only the current condition is predicted. If None, :var:`config.conditionswitch_predict_all` is used. :: image jill = ConditionSwitch( "jill_beers > 4", "jill_drunk.png", "True", "jill_sober.png") """ predict_all = kwargs.pop("predict_all", None) 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]): if cond not in cond_cache: code = renpy.python.py_compile(cond, 'eval') cond_cache[cond] = code d = renpy.easy.displayable(d) switch.append((cond, d)) rv = DynamicDisplayable(condition_switch_show, switch, predict_all, _predict_function=condition_switch_predict) return Position(rv, **kwargs) def ShowingSwitch(*args, **kwargs): """ :doc: disp_dynamic :args: (*args, predict_all=None, **properties) 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. `predict_all` If True, all of the possible displayables will be predicted when the displayable is shown. If False, only the current condition is predicted. If None, :var:`config.conditionswitch_predict_all` is used. 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 Crop(rect, child, **properties): """ :doc: disp_imagelike :name: Crop This creates a displayable by cropping `child` to `rect`, where `rect` is an (x, y, width, height) tuple. :: image eileen cropped = Crop((0, 0, 300, 300), "eileen happy") """ return renpy.display.motion.Transform(child, crop=rect, **properties) LiveCrop = Crop 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, basestring): positions = positions.split() seen = set() for i in positions: if not i in Side.possible_positions: raise Exception("Side used with impossible position '%s'." % (i,)) if i in seen: raise Exception("Side used with duplicate position '%s'." % (i,)) seen.add(i) self.positions = tuple(positions) self.sized = False def add(self, d): if len(self.children) >= len(self.positions): raise Exception("Side has been given too many arguments.") super(Side, self).add(d) def _clear(self): super(Side, self)._clear() self.sized = False def per_interact(self): self.sized = False def render(self, width, height, st, at): if renpy.config.developer and len(self.positions) != len(self.children): raise Exception("A side has the wrong number of children.") 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). old_width = width old_height = height 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 = renpy.display.render.render_for_size(pos_d[pos], width, height, st, at) return max(owidth, rend.width), max(oheight, rend.height) 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. width = old_width height = old_height self.offsets = [ None ] * len(self.children) lefts = self.left_space rights = self.right_space tops = self.top_space bottoms = self.bottom_space if self.style.xfill: cwidth = width if self.style.yfill: cheight = height 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_order = [ ('c', col2, row2, cwidth, cheight), ('t', col2, row1, cwidth, top), ('r', col3, row2, right, cheight), ('b', col2, row3, cwidth, bottom), ('l', col1, row2, left, cheight), ('tl', col1, row1, left, top), ('tr', col3, row1, right, top), ('br', col3, row3, right, bottom), ('bl', col1, row3, left, bottom), ] # This sorts the children for placement according to # their order in positions. if renpy.config.keep_side_render_order: def sort(elem): pos, x, y, w, h = elem if pos not in pos_d: return return self.positions.index(pos) place_order.sort(key=sort) for pos, x, y, w, h in place_order: place(pos, x, y, w, h) 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 adjusted_times(self): interact_time = renpy.game.interface.interact_time if (self.start_time is None) and (interact_time is not None): self.start_time = interact_time if self.start_time is not None: st = renpy.game.interface.frame_time - self.start_time else: st = 0 if (self.anim_time is None) and (interact_time is not None): self.anim_time = interact_time if self.anim_time is not None: at = renpy.game.interface.frame_time - self.anim_time else: at = 0 return st, at def render(self, w, h, st, at): st, at = self.adjusted_times() 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 event(self, ev, x, y, st): st, _ = self.adjusted_times() Container.event(self, ev, x, y, st) def get_placement(self): return self.child.get_placement() class Tile(Container): """ :doc: disp_imagelike :name: Tile Tiles `child` until it fills the area allocated to this displayable. :: image bg tile = Tile("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): ccw = min(cw, width - x) cch = min(ch, height - y) if (ccw < cw) or (cch < ch): ccr = cr.subsurface((0, 0, ccw, cch)) else: ccr = cr rv.blit(ccr, (x, y), focus=False) return rv LiveTile = Tile class Flatten(Container): """ :doc: disp_imagelike This flattens `child`, which may be made up of multiple textures, into a single texture. Certain operations, like the alpha transform property, apply to every texture making up a displayable, which can yield incorrect results when the textures overlap on screen. Flatten creates a single texture from multiple textures, which can prevent this problem. Flatten is a relatively expensive operation, and so should only be used when absolutely required. """ def __init__(self, child, **properties): super(Flatten, self).__init__(**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(cw, ch) rv.blit(cr, (0, 0)) rv.operation = renpy.display.render.FLATTEN rv.mesh = True rv.shaders = ( "renpy.texture", ) self.offsets = [ (0, 0) ] return rv def get_placement(self): return self.child.get_placement() class AlphaMask(Container): """ :doc: disp_imagelike This displayable takes its colors from `child`, and its alpha channel from the multiplication of the alpha channels of `child` and `mask`. The result is a displayable that has the same colors as `child`, is transparent where either `child` or `mask` is transparent, and is opaque where `child` and `mask` are both opaque. The `child` and `mask` parameters may be arbitrary displayables. The size of the AlphaMask is the size of `child`. """ def __init__(self, child, mask, **properties): super(AlphaMask, self).__init__(**properties) self.add(child) self.mask = renpy.easy.displayable(mask) self.null = None def render(self, width, height, st, at): cr = renpy.display.render.render(self.child, width, height, st, at) w, h = cr.get_size() mr = renpy.display.render.Render(w, h) mr.place(self.mask, main=False) if self.null is None: self.null = Fixed() nr = renpy.display.render.render(self.null, w, h, st, at) rv = renpy.display.render.Render(w, h, opaque=False) rv.operation = renpy.display.render.IMAGEDISSOLVE rv.operation_alpha = 1.0 rv.operation_complete = 256.0 / (256.0 + 256.0) rv.operation_parameter = 256 rv.blit(mr, (0, 0), focus=False, main=False) rv.blit(nr, (0, 0), focus=False, main=False) rv.blit(cr, (0, 0)) return rv