1# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us>
2#
3# Permission is hereby granted, free of charge, to any person
4# obtaining a copy of this software and associated documentation files
5# (the "Software"), to deal in the Software without restriction,
6# including without limitation the rights to use, copy, modify, merge,
7# publish, distribute, sublicense, and/or sell copies of the Software,
8# and to permit persons to whom the Software is furnished to do so,
9# subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be
12# included in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22# This file contains classes that handle layout of displayables on
23# the screen.
24
25from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
26from renpy.compat import *
27
28import math
29
30import renpy.display
31import pygame_sdl2 as pygame
32
33
34def edgescroll_proportional(n):
35    """
36    An edgescroll function that causes the move speed to be proportional
37    from the edge distance.
38    """
39    return n
40
41
42class Viewport(renpy.display.layout.Container):
43
44    __version__ = 5
45
46    arrowkeys = False
47    pagekeys = False
48
49    def after_upgrade(self, version):
50        if version < 1:
51            self.xadjustment = renpy.display.behavior.Adjustment(1, 0)
52            self.yadjustment = renpy.display.behavior.Adjustment(1, 0)
53            self.set_adjustments = False
54            self.mousewheel = False
55            self.draggable = False
56            self.width = 0
57            self.height = 0
58
59        if version < 2:
60            self.drag_position = None
61
62        if version < 3:
63            self.edge_size = False
64            self.edge_speed = False
65            self.edge_function = None
66            self.edge_xspeed = 0
67            self.edge_yspeed = 0
68            self.edge_last_st = None
69
70        if version < 5:
71            self.focusable = self.draggable
72
73    def __init__(self,
74                 child=None,
75                 child_size=(None, None),
76                 offsets=(None, None),
77                 xadjustment=None,
78                 yadjustment=None,
79                 set_adjustments=True,
80                 mousewheel=False,
81                 draggable=False,
82                 edgescroll=None,
83                 style='viewport',
84                 xinitial=None,
85                 yinitial=None,
86                 replaces=None,
87                 arrowkeys=False,
88                 pagekeys=False,
89                 **properties):
90
91        super(Viewport, self).__init__(style=style, **properties)
92
93        if child is not None:
94            self.add(child)
95
96        if xadjustment is None:
97            self.xadjustment = renpy.display.behavior.Adjustment(1, 0)
98        else:
99            self.xadjustment = xadjustment
100
101        if yadjustment is None:
102            self.yadjustment = renpy.display.behavior.Adjustment(1, 0)
103        else:
104            self.yadjustment = yadjustment
105
106        if self.xadjustment.adjustable is None:
107            self.xadjustment.adjustable = True
108
109        if self.yadjustment.adjustable is None:
110            self.yadjustment.adjustable = True
111
112        self.set_adjustments = set_adjustments
113
114        self.xoffset = offsets[0] if (offsets[0] is not None) else xinitial
115        self.yoffset = offsets[1] if (offsets[1] is not None) else yinitial
116
117        if isinstance(replaces, Viewport) and replaces.offsets:
118            self.xadjustment.range = replaces.xadjustment.range
119            self.xadjustment.value = replaces.xadjustment.value
120            self.yadjustment.range = replaces.yadjustment.range
121            self.yadjustment.value = replaces.yadjustment.value
122            self.xoffset = replaces.xoffset
123            self.yoffset = replaces.yoffset
124            self.drag_position = replaces.drag_position
125        else:
126            self.drag_position = None
127
128        self.child_width, self.child_height = child_size
129
130        self.mousewheel = mousewheel
131        self.draggable = draggable
132        self.arrowkeys = arrowkeys
133        self.pagekeys = pagekeys
134
135        # Layout participates in the focus system so drags get migrated.
136        self.focusable = draggable or arrowkeys
137
138        self.width = 0
139        self.height = 0
140
141        # The speed at which we scroll in the x and y directions, in pixels
142        # per second.
143        self.edge_xspeed = 0
144        self.edge_yspeed = 0
145
146        # The last time we edgescrolled.
147        self.edge_last_st = None
148
149        if edgescroll is not None:
150
151            # The size of the edges that trigger scrolling.
152            self.edge_size = edgescroll[0]
153
154            # How far from the edge we can scroll.
155            self.edge_speed = edgescroll[1]
156
157            if len(edgescroll) >= 3:
158                self.edge_function = edgescroll[2]
159            else:
160                self.edge_function = edgescroll_proportional
161
162        else:
163            self.edge_size = 0
164            self.edge_speed = 0
165            self.edge_function = edgescroll_proportional
166
167    def per_interact(self):
168        self.xadjustment.register(self)
169        self.yadjustment.register(self)
170
171    def update_offsets(self, cw, ch, st):
172        """
173        This is called by render once we know the width (`cw`) and height (`ch`)
174        of all the children. It returns a pair of offsets that should be applied
175        to all children.
176
177        It also requires `st`, since hit handles edge scrolling.
178
179        The returned offsets will be negative or zero.
180        """
181
182        cw = int(math.ceil(cw))
183        ch = int(math.ceil(ch))
184
185        width = self.width
186        height = self.height
187
188        xminimum, yminimum = renpy.display.layout.xyminimums(self.style, width, height)
189
190        if not self.style.xfill:
191            width = min(cw, width)
192
193        if not self.style.yfill:
194            height = min(ch, height)
195
196        width = max(width, xminimum)
197        height = max(height, yminimum)
198
199        if (not renpy.display.render.sizing) and self.set_adjustments:
200
201            xarange = max(cw - width, 0)
202
203            if (self.xadjustment.range != xarange) or (self.xadjustment.page != width):
204                self.xadjustment.range = xarange
205                self.xadjustment.page = width
206                self.xadjustment.update()
207
208            yarange = max(ch - height, 0)
209
210            if (self.yadjustment.range != yarange) or (self.yadjustment.page != height):
211                self.yadjustment.range = yarange
212                self.yadjustment.page = height
213                self.yadjustment.update()
214
215        if self.xoffset is not None:
216            if isinstance(self.xoffset, int):
217                value = self.xoffset
218            else:
219                value = max(cw - width, 0) * self.xoffset
220
221            self.xadjustment.value = value
222
223        if self.yoffset is not None:
224            if isinstance(self.yoffset, int):
225                value = self.yoffset
226            else:
227                value = max(ch - height, 0) * self.yoffset
228
229            self.yadjustment.value = value
230
231        if self.edge_size and (self.edge_last_st is not None) and (self.edge_xspeed or self.edge_yspeed):
232
233            duration = max(st - self.edge_last_st, 0)
234            self.xadjustment.change(self.xadjustment.value + duration * self.edge_xspeed)
235            self.yadjustment.change(self.yadjustment.value + duration * self.edge_yspeed)
236
237            self.check_edge_redraw(st)
238
239        cxo = -int(self.xadjustment.value)
240        cyo = -int(self.yadjustment.value)
241
242        self._clipping = (cw > width) or (ch > height)
243
244        self.width = width
245        self.height = height
246
247        return cxo, cyo, width, height
248
249    def render(self, width, height, st, at):
250
251        self.width = width
252        self.height = height
253
254        child_width = self.child_width or width
255        child_height = self.child_height or height
256
257        surf = renpy.display.render.render(self.child, child_width, child_height, st, at)
258
259        cw, ch = surf.get_size()
260        cxo, cyo, width, height = self.update_offsets(cw, ch, st)
261
262        self.offsets = [ (cxo, cyo) ]
263
264        rv = renpy.display.render.Render(width, height)
265        rv.blit(surf, (cxo, cyo))
266
267        rv = rv.subsurface((0, 0, width, height), focus=True)
268
269        if self.draggable or self.arrowkeys:
270            rv.add_focus(self, None, 0, 0, width, height)
271
272        return rv
273
274    def check_edge_redraw(self, st, reset_st=True):
275        redraw = False
276
277        if (self.edge_xspeed > 0) and (self.xadjustment.value < self.xadjustment.range):
278            redraw = True
279        if (self.edge_xspeed < 0) and (self.xadjustment.value > 0):
280            redraw = True
281
282        if (self.edge_yspeed > 0) and (self.yadjustment.value < self.yadjustment.range):
283            redraw = True
284        if (self.edge_yspeed < 0) and (self.yadjustment.value > 0):
285            redraw = True
286
287        if redraw:
288            renpy.display.render.redraw(self, 0)
289            if reset_st or self.edge_last_st is None:
290                self.edge_last_st = st
291        else:
292            self.edge_last_st = None
293
294    def event(self, ev, x, y, st):
295
296        self.xoffset = None
297        self.yoffset = None
298
299        rv = super(Viewport, self).event(ev, x, y, st)
300
301        if rv is not None:
302            return rv
303
304        if self.draggable and renpy.display.focus.get_grab() == self:
305
306            old_xvalue = self.xadjustment.value
307            old_yvalue = self.yadjustment.value
308
309            if renpy.display.behavior.map_event(ev, 'viewport_drag_end'):
310                renpy.display.focus.set_grab(None)
311
312                # Invoke rounding adjustment on viewport release
313                xvalue = self.xadjustment.round_value(old_xvalue, release=True)
314                self.xadjustment.change(xvalue)
315                yvalue = self.yadjustment.round_value(old_yvalue, release=True)
316                self.yadjustment.change(yvalue)
317                raise renpy.display.core.IgnoreEvent()
318
319            oldx, oldy = self.drag_position
320            dx = x - oldx
321            dy = y - oldy
322
323            new_xvalue = self.xadjustment.round_value(old_xvalue - dx, release=False)
324            if old_xvalue == new_xvalue:
325                newx = oldx
326            else:
327                self.xadjustment.change(new_xvalue)
328                newx = x
329
330            new_yvalue = self.yadjustment.round_value(old_yvalue - dy, release=False)
331            if old_yvalue == new_yvalue:
332                newy = oldy
333            else:
334                self.yadjustment.change(new_yvalue)
335                newy = y
336
337            self.drag_position = (newx, newy) # W0201
338
339        if not ((0 <= x < self.width) and (0 <= y <= self.height)):
340            self.edge_xspeed = 0
341            self.edge_yspeed = 0
342            self.edge_last_st = None
343
344            inside = False
345
346        else:
347
348            inside = True
349
350        if inside and self.mousewheel:
351
352            if self.mousewheel == "horizontal-change":
353                adjustment = self.xadjustment
354                change = True
355            elif self.mousewheel == "change":
356                adjustment = self.yadjustment
357                change = True
358            elif self.mousewheel == "horizontal":
359                adjustment = self.xadjustment
360                change = False
361            else:
362                adjustment = self.yadjustment
363                change = False
364
365            if renpy.display.behavior.map_event(ev, 'viewport_wheelup'):
366
367                if change and (adjustment.value == 0):
368                    return None
369
370                rv = adjustment.change(adjustment.value - adjustment.step)
371                if rv is not None:
372                    return rv
373                else:
374                    raise renpy.display.core.IgnoreEvent()
375
376            if renpy.display.behavior.map_event(ev, 'viewport_wheeldown'):
377
378                if change and (adjustment.value == adjustment.range):
379                    return None
380
381                rv = adjustment.change(adjustment.value + adjustment.step)
382                if rv is not None:
383                    return rv
384                else:
385                    raise renpy.display.core.IgnoreEvent()
386
387        if self.arrowkeys:
388
389            if renpy.display.behavior.map_event(ev, 'viewport_leftarrow'):
390
391                if self.xadjustment.value == 0:
392                    return None
393
394                rv = self.xadjustment.change(self.xadjustment.value - self.xadjustment.step)
395                if rv is not None:
396                    return rv
397                else:
398                    raise renpy.display.core.IgnoreEvent()
399
400            if renpy.display.behavior.map_event(ev, 'viewport_rightarrow'):
401
402                if self.xadjustment.value == self.xadjustment.range:
403                    return None
404
405                rv = self.xadjustment.change(self.xadjustment.value + self.xadjustment.step)
406                if rv is not None:
407                    return rv
408                else:
409                    raise renpy.display.core.IgnoreEvent()
410
411            if renpy.display.behavior.map_event(ev, 'viewport_uparrow'):
412
413                if self.yadjustment.value == 0:
414                    return None
415
416                rv = self.yadjustment.change(self.yadjustment.value - self.yadjustment.step)
417                if rv is not None:
418                    return rv
419                else:
420                    raise renpy.display.core.IgnoreEvent()
421
422            if renpy.display.behavior.map_event(ev, 'viewport_downarrow'):
423
424                if self.yadjustment.value == self.yadjustment.range:
425                    return None
426
427                rv = self.yadjustment.change(self.yadjustment.value + self.yadjustment.step)
428                if rv is not None:
429                    return rv
430                else:
431                    raise renpy.display.core.IgnoreEvent()
432
433        if self.pagekeys:
434
435            if renpy.display.behavior.map_event(ev, 'viewport_pageup'):
436
437                rv = self.yadjustment.change(self.yadjustment.value - self.yadjustment.page)
438                if rv is not None:
439                    return rv
440                else:
441                    raise renpy.display.core.IgnoreEvent()
442
443            if renpy.display.behavior.map_event(ev, 'viewport_pagedown'):
444
445                rv = self.yadjustment.change(self.yadjustment.value + self.yadjustment.page)
446                if rv is not None:
447                    return rv
448                else:
449                    raise renpy.display.core.IgnoreEvent()
450
451        if inside and self.draggable:
452
453            if renpy.display.behavior.map_event(ev, 'viewport_drag_start'):
454
455                focused = renpy.display.focus.get_focused()
456
457                if (focused is None) or (focused is self):
458
459                    self.drag_position = (x, y)
460                    renpy.display.focus.set_grab(self)
461                    raise renpy.display.core.IgnoreEvent()
462
463        if inside and self.edge_size and ev.type in [ pygame.MOUSEMOTION, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP ]:
464
465            def speed(n, zero, one):
466                """
467                Given a position `n`, computes the speed. The speed is 0.0
468                when `n` == `zero`, 1.0 when `n` == `one`, and linearly
469                interpolated when between.
470
471                Returns 0.0 when outside the bounds - in either direction.
472                """
473
474                n = 1.0 * (n - zero) / (one - zero)
475
476                if n < 0.0:
477                    return 0.0
478                if n > 1.0:
479                    return 0.0
480
481                return n
482
483            xspeed = speed(x, self.width - self.edge_size, self.width)
484            xspeed -= speed(x, self.edge_size, 0)
485            self.edge_xspeed = self.edge_speed * self.edge_function(xspeed)
486
487            yspeed = speed(y, self.height - self.edge_size, self.height)
488            yspeed -= speed(y, self.edge_size, 0)
489            self.edge_yspeed = self.edge_speed * self.edge_function(yspeed)
490
491            if xspeed or yspeed:
492                self.check_edge_redraw(st, reset_st=False)
493            else:
494                self.edge_last_st = None
495
496        return None
497
498    def set_xoffset(self, offset):
499        self.xoffset = offset
500        renpy.display.render.redraw(self, 0)
501
502    def set_yoffset(self, offset):
503        self.yoffset = offset
504        renpy.display.render.redraw(self, 0)
505
506
507# For compatibility with old saves.
508renpy.display.layout.Viewport = Viewport
509
510
511class VPGrid(Viewport):
512
513    __version__ = Viewport.__version__
514
515    def __init__(self, cols=None, rows=None, transpose=None, style="vpgrid", **properties):
516
517        super(VPGrid, self).__init__(style=style, **properties)
518
519        if (rows is None) and (cols is None):
520            raise Exception("A VPGrid must be given the rows or cols property.")
521
522        if (rows is not None) and (cols is None) and (transpose is None):
523            transpose = True
524
525        self.grid_cols = cols
526        self.grid_rows = rows
527        self.grid_transpose = transpose
528
529    def render(self, width, height, st, at):
530
531        self.width = width
532        self.height = height
533
534        child_width = self.child_width or width
535        child_height = self.child_height or height
536
537        if not self.children:
538            self.offsets = [ ]
539            return renpy.display.render.Render(0, 0)
540
541        # The number of children.
542        lc = len(self.children)
543
544        # Figure out the number of columns and rows.
545        cols = self.grid_cols
546        rows = self.grid_rows
547
548        if cols is None:
549            cols = lc // rows
550            if rows * cols < lc:
551                cols += 1
552
553        if rows is None:
554            rows = lc // cols
555            if rows * cols < lc:
556                rows += 1
557
558        # Determine the total size.
559        xspacing = self.style.xspacing
560        yspacing = self.style.yspacing
561
562        if xspacing is None:
563            xspacing = self.style.spacing
564        if yspacing is None:
565            yspacing = self.style.spacing
566
567        left_margin = renpy.display.layout.scale(self.style.left_margin, width)
568        right_margin = renpy.display.layout.scale(self.style.right_margin, width)
569        top_margin = renpy.display.layout.scale(self.style.top_margin, height)
570        bottom_margin = renpy.display.layout.scale(self.style.bottom_margin, height)
571
572        rend = renpy.display.render.render(self.children[0], child_width, child_height, st, at)
573        cw, ch = rend.get_size()
574
575        tw = (cw + xspacing) * cols - xspacing + left_margin + right_margin
576        th = (ch + yspacing) * rows - yspacing + top_margin + bottom_margin
577
578        if self.style.xfill:
579            tw = child_width
580            cw = (tw - (cols - 1) * xspacing - left_margin - right_margin) // cols
581
582        if self.style.yfill:
583            th = child_height
584            ch = (th - (rows - 1) * yspacing - top_margin - bottom_margin) // rows
585
586        cxo, cyo, width, height = self.update_offsets(tw, th, st)
587        cxo += left_margin
588        cyo += top_margin
589
590        self.offsets = [ ]
591
592        # Render everything.
593        rv = renpy.display.render.Render(width, height)
594
595        for index, c in enumerate(self.children):
596
597            if self.grid_transpose:
598                x = index // rows
599                y = index % rows
600            else:
601                x = index % cols
602                y = index // cols
603
604            x = x * (cw + xspacing) + cxo
605            y = y * (ch + yspacing) + cyo
606
607            if x + cw < 0:
608                self.offsets.append((x, y))
609                continue
610
611            if y + ch < 0:
612                self.offsets.append((x, y))
613                continue
614
615            if x >= width:
616                self.offsets.append((x, y))
617                continue
618
619            if y >= height:
620                self.offsets.append((x, y))
621                continue
622
623            surf = renpy.display.render.render(c, cw, ch, st, at)
624            pos = c.place(rv, x, y, cw, ch, surf)
625
626            self.offsets.append(pos)
627
628        rv = rv.subsurface((0, 0, width, height), focus=True)
629
630        if self.draggable or self.arrowkeys:
631            rv.add_focus(self, None, 0, 0, width, height)
632
633        return rv
634