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