# 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 import renpy.display import pygame_sdl2 as pygame def edgescroll_proportional(n): """ An edgescroll function that causes the move speed to be proportional from the edge distance. """ return n class Viewport(renpy.display.layout.Container): __version__ = 5 arrowkeys = False pagekeys = False 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 if version < 4: self.xadjustment_param = None self.yadjustment_param = None self.offsets_param = (None, None) self.set_adjustments_param = True self.xinitial_param = None self.yinitial_param = None if version < 5: self.focusable = self.draggable 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, arrowkeys=False, pagekeys=False, **properties): super(Viewport, self).__init__(style=style, **properties) if child is not None: self.add(child) self.xadjustment_param = xadjustment self.yadjustment_param = yadjustment self.offsets_param = offsets self.set_adjustments_param = set_adjustments self.xinitial_param = xinitial self.yinitial_param = yinitial self._show() if isinstance(replaces, Viewport) and replaces.offsets: self.xadjustment.range = replaces.xadjustment.range self.xadjustment.value = replaces.xadjustment.value self.yadjustment.range = replaces.yadjustment.range self.yadjustment.value = replaces.yadjustment.value self.xoffset = replaces.xoffset self.yoffset = replaces.yoffset self.drag_position = replaces.drag_position else: self.drag_position = None self.child_width, self.child_height = child_size self.mousewheel = mousewheel self.draggable = draggable self.arrowkeys = arrowkeys self.pagekeys = pagekeys # Layout participates in the focus system so drags get migrated. self.focusable = draggable or arrowkeys 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 _show(self): if self.xadjustment_param is None: self.xadjustment = renpy.display.behavior.Adjustment(1, 0) else: self.xadjustment = self.xadjustment_param if self.yadjustment_param is None: self.yadjustment = renpy.display.behavior.Adjustment(1, 0) else: self.yadjustment = self.yadjustment_param if self.xadjustment.adjustable is None: self.xadjustment.adjustable = True if self.yadjustment.adjustable is None: self.yadjustment.adjustable = True self.set_adjustments = self.set_adjustments_param offsets = self.offsets_param self.xoffset = offsets[0] if (offsets[0] is not None) else self.xinitial_param self.yoffset = offsets[1] if (offsets[1] is not None) else self.yinitial_param def per_interact(self): self.xadjustment.register(self) self.yadjustment.register(self) def update_offsets(self, cw, ch, st): """ This is called by render once we know the width (`cw`) and height (`ch`) of all the children. It returns a pair of offsets that should be applied to all children. It also requires `st`, since hit handles edge scrolling. The returned offsets will be negative or zero. """ width = self.width height = self.height if not self.style.xfill: width = min(cw, width) if not self.style.yfill: height = min(ch, height) width = max(width, self.style.xminimum) height = max(height, self.style.yminimum) if (not renpy.display.render.sizing) and self.set_adjustments: xarange = max(cw - width, 0) if (self.xadjustment.range != xarange) or (self.xadjustment.page != width): self.xadjustment.range = xarange self.xadjustment.page = width self.xadjustment.update() yarange = max(ch - height, 0) if (self.yadjustment.range != yarange) or (self.yadjustment.page != height): self.yadjustment.range = yarange self.yadjustment.page = height self.yadjustment.update() 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 is not None) 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(st) cxo = -int(self.xadjustment.value) cyo = -int(self.yadjustment.value) self._clipping = (cw > width) or (ch > height) return cxo, cyo, width, height 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 = renpy.display.render.render(self.child, child_width, child_height, st, at) cw, ch = surf.get_size() cxo, cyo, width, height = self.update_offsets(cw, ch, st) self.offsets = [ (cxo, cyo) ] rv = renpy.display.render.Render(width, height) rv.blit(surf, (cxo, cyo)) rv = rv.subsurface((0, 0, width, height), focus=True) if self.draggable or self.arrowkeys: rv.add_focus(self, None, 0, 0, width, height) return rv def check_edge_redraw(self, st, reset_st=True): 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) if reset_st or self.edge_last_st is None: self.edge_last_st = st else: self.edge_last_st = None 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: old_xvalue = self.xadjustment.value old_yvalue = self.yadjustment.value if renpy.display.behavior.map_event(ev, 'viewport_drag_end'): renpy.display.focus.set_grab(None) # Invoke rounding adjustment on viewport release xvalue = self.xadjustment.round_value(old_xvalue, release=True) self.xadjustment.change(xvalue) yvalue = self.yadjustment.round_value(old_yvalue, release=True) self.yadjustment.change(yvalue) raise renpy.display.core.IgnoreEvent() oldx, oldy = self.drag_position dx = x - oldx dy = y - oldy new_xvalue = self.xadjustment.round_value(old_xvalue - dx, release=False) if old_xvalue == new_xvalue: newx = oldx else: self.xadjustment.change(new_xvalue) newx = x new_yvalue = self.yadjustment.round_value(old_yvalue - dy, release=False) if old_yvalue == new_yvalue: newy = oldy else: self.yadjustment.change(new_yvalue) newy = y self.drag_position = (newx, newy) # W0201 if not ((0 <= x < self.width) and (0 <= y <= self.height)): self.edge_xspeed = 0 self.edge_yspeed = 0 self.edge_last_st = None inside = False else: inside = True if inside and self.mousewheel: if self.mousewheel == "horizontal-change": adjustment = self.xadjustment change = True elif self.mousewheel == "change": adjustment = self.yadjustment change = True elif self.mousewheel == "horizontal": adjustment = self.xadjustment change = False else: adjustment = self.yadjustment change = False if renpy.display.behavior.map_event(ev, 'viewport_up'): if change and (adjustment.value == 0): return None rv = adjustment.change(adjustment.value - adjustment.step) if rv is not None: return rv else: raise renpy.display.core.IgnoreEvent() if renpy.display.behavior.map_event(ev, 'viewport_down'): if change and (adjustment.value == adjustment.range): return None rv = adjustment.change(adjustment.value + adjustment.step) if rv is not None: return rv else: raise renpy.display.core.IgnoreEvent() if self.arrowkeys: if renpy.display.behavior.map_event(ev, 'viewport_leftarrow'): if self.xadjustment.value == 0: return None rv = self.xadjustment.change(self.xadjustment.value - self.xadjustment.step) if rv is not None: return rv else: raise renpy.display.core.IgnoreEvent() if renpy.display.behavior.map_event(ev, 'viewport_rightarrow'): if self.xadjustment.value == self.xadjustment.range: return None rv = self.xadjustment.change(self.xadjustment.value + self.xadjustment.step) if rv is not None: return rv else: raise renpy.display.core.IgnoreEvent() if renpy.display.behavior.map_event(ev, 'viewport_uparrow'): if self.yadjustment.value == 0: return None 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_downarrow'): if self.yadjustment.value == self.yadjustment.range: return None 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.pagekeys: if renpy.display.behavior.map_event(ev, 'viewport_pageup'): rv = self.yadjustment.change(self.yadjustment.value - self.yadjustment.page) if rv is not None: return rv else: raise renpy.display.core.IgnoreEvent() if renpy.display.behavior.map_event(ev, 'viewport_pagedown'): rv = self.yadjustment.change(self.yadjustment.value + self.yadjustment.page) if rv is not None: return rv else: raise renpy.display.core.IgnoreEvent() if inside and self.draggable: if renpy.display.behavior.map_event(ev, 'viewport_drag_start'): focused = renpy.display.focus.get_focused() if (focused is None) or (focused is self): self.drag_position = (x, y) renpy.display.focus.set_grab(self) raise renpy.display.core.IgnoreEvent() if inside and self.edge_size and ev.type in [ pygame.MOUSEMOTION, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP ]: 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(st, reset_st=False) 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) # For compatibility with old saves. renpy.display.layout.Viewport = Viewport class VPGrid(Viewport): __version__ = Viewport.__version__ def __init__(self, cols=None, rows=None, transpose=None, style="vpgrid", **properties): super(VPGrid, self).__init__(style=style, **properties) if (rows is None) and (cols is None): raise Exception("A VPGrid must be given the rows or cols property.") if (rows is not None) and (cols is None) and (transpose is None): transpose = True self.grid_cols = cols self.grid_rows = rows self.grid_transpose = transpose 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 if not self.children: self.offsets = [ ] return renpy.display.render.Render(0, 0) # The number of children. lc = len(self.children) # Figure out the number of columns and rows. cols = self.grid_cols rows = self.grid_rows if cols is None: cols = lc // rows if rows * cols < lc: cols += 1 if rows is None: rows = lc // cols if rows * cols < lc: rows += 1 # Determine the total size. xspacing = self.style.xspacing yspacing = self.style.yspacing if xspacing is None: xspacing = self.style.spacing if yspacing is None: yspacing = self.style.spacing rend = renpy.display.render.render(self.children[0], child_width, child_height, st, at) cw, ch = rend.get_size() tw = (cw + xspacing) * cols - xspacing th = (ch + yspacing) * rows - yspacing if self.style.xfill: tw = child_width cw = (tw - (cols - 1) * xspacing) / cols if self.style.yfill: th = child_height ch = (th - (rows - 1) * yspacing) / rows cxo, cyo, width, height = self.update_offsets(tw, th, st) self.offsets = [ ] # Render everything. rv = renpy.display.render.Render(width, height) for index, c in enumerate(self.children): if self.grid_transpose: x = index // rows y = index % rows else: x = index % cols y = index // cols x = x * (cw + xspacing) + cxo y = y * (ch + yspacing) + cyo if x + cw < 0: self.offsets.append((x, y)) continue if y + ch < 0: self.offsets.append((x, y)) continue if x >= width: self.offsets.append((x, y)) continue if y >= height: self.offsets.append((x, y)) continue surf = renpy.display.render.render(c, cw, ch, st, at) pos = c.place(rv, x, y, cw, ch, surf) self.offsets.append(pos) rv = rv.subsurface((0, 0, width, height), focus=True) if self.draggable or self.arrowkeys: rv.add_focus(self, None, 0, 0, width, height) return rv