]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Scrollable pad
authorAxy <gilliardmarthey.axel@gmail.com>
Wed, 4 Mar 2026 01:21:25 +0000 (02:21 +0100)
committerAxy <gilliardmarthey.axel@gmail.com>
Wed, 4 Mar 2026 01:21:25 +0000 (02:21 +0100)
__main__.py
amazeing/maze_display/TTYdisplay.py
amazeing/maze_display/backend.py

index 238d6398950a632d4d3487152f2734761098d500..ce4892440d8952857b4f795181a9ee95ad40dfa1 100644 (file)
@@ -1,4 +1,5 @@
 import curses
+import time
 from amazeing import (
     Maze,
     TTYBackend,
@@ -12,7 +13,7 @@ from sys import stderr, stdin
 from amazeing.config.config_parser import Config
 from amazeing.maze_class.maze_walls import Cardinal, CellCoord
 from amazeing.maze_display.TTYdisplay import TTYTile
-from amazeing.maze_display.backend import IVec2
+from amazeing.maze_display.backend import BackendEvent, CloseRequested, IVec2
 
 # from amazeing.maze_display.layout import example
 
@@ -74,8 +75,23 @@ def display_maze(maze: Maze) -> None:
         for pixel in wall.tile_coords():
             backend.draw_tile(pixel)
     backend.present()
-    if backend.event(0) is not None:
-        exit()
+    poll_events(0)
+
+
+def poll_events(timeout_ms: int = -1) -> None:
+    start = time.monotonic()
+    elapsed_ms = lambda: int((time.monotonic() - start) * 1000.0)
+    timeout = lambda: (
+        max(timeout_ms - elapsed_ms(), 0) if timeout_ms != -1 else -1
+    )
+    while True:
+        event = backend.event(timeout())
+        if event is None:
+            if timeout_ms == -1:
+                continue
+            return
+        if isinstance(event, CloseRequested) or event.sym == "q":
+            exit(0)
 
 
 maze_make_perfect(maze, callback=display_maze)
@@ -84,5 +100,4 @@ while False:
     maze_make_perfect(maze, callback=display_maze)
     maze_make_pacman(maze, walls_const, callback=display_maze)
     maze._rebuild()
-while backend.event(-1) is None:
-    backend.present()
+poll_events()
index 0ec946156f4db07f5a2315e248fdc0c43e5ad3f6..fca9e2e45d7ebdac5c57caf4a2f70ad37ed6ceb1 100644 (file)
@@ -1,3 +1,4 @@
+from sys import stderr
 from amazeing.maze_display.layout import (
     BInt,
     Box,
@@ -25,7 +26,10 @@ class TTYTile:
             for x, (char, attrs) in enumerate(
                 line[src.x : src.x + size.x]  # noqa E203
             ):
-                window.addch(dst.y + y, dst.x + x, char, attrs)
+                try:
+                    window.addch(dst.y + y, dst.x + x, char, attrs)
+                except curses.error:
+                    pass  # dumb exception when writing bottom right corner
 
 
 class TTYTileMap:
@@ -58,7 +62,51 @@ class TTYTileMap:
 
 
 class ScrollablePad:
-    pass
+    def __init__(
+        self,
+        dims: IVec2,
+        constrained: bool = True,
+        init_pos: IVec2 = IVec2.splat(0),
+    ) -> None:
+        self.__pos = init_pos
+        self.pad: curses.window = curses.newpad(dims.y, dims.x)
+        self.constrained = constrained
+
+    def dims(self) -> IVec2:
+        y, x = self.pad.getmaxyx()
+        return IVec2(x, y)
+
+    def clamp(self, dims: IVec2) -> None:
+        self.__pos = IVec2.with_op(min)(
+            IVec2.with_op(max)(self.__pos, dims - self.dims()), IVec2.splat(0)
+        )
+
+    def refresh(self, at: IVec2, into: IVec2) -> None:
+        if self.constrained:
+            self.clamp(into)
+
+        pad_start = IVec2.with_op(max)(
+            IVec2.splat(0) - self.__pos, IVec2.splat(0)
+        )
+        win_start = IVec2.with_op(max)(self.__pos, IVec2.splat(0))
+        draw_dim = IVec2.with_op(min)(
+            self.dims() - pad_start, into - win_start
+        )
+        if draw_dim.x <= 0 or draw_dim.y <= 0:
+            return
+        draw_start = at + win_start
+        draw_end = draw_start + draw_dim - IVec2.splat(1)
+        self.pad.refresh(
+            *pad_start.yx(),
+            *draw_start.yx(),
+            *draw_end.yx(),
+        )
+
+    def move(self, by: IVec2) -> None:
+        self.__pos = self.__pos + by
+
+    def scroll(self, by: IVec2) -> None:
+        self.move(by * IVec2.splat(-1))
 
 
 class TTYBackend(Backend[int]):
@@ -82,14 +130,12 @@ class TTYBackend(Backend[int]):
         curses.curs_set(0)
         self.__screen.keypad(True)
 
-        self.__pad: curses.window = curses.newpad(dims.y + 1, dims.x + 1)
+        self.__pad: ScrollablePad = ScrollablePad(dims)
         self.__dims = maze_dims
 
         maze_box = FBox(
             IVec2(BInt(dims.x), BInt(dims.y)),
-            lambda at, into: self.__pad.refresh(
-                0, 0, at.y, at.x, at.y + into.y - 1, at.x + into.x - 1
-            ),
+            self.__pad.refresh,
         )
         self.__layout: Box = VBox.noassoc(
             layout_fair,
@@ -114,12 +160,13 @@ class TTYBackend(Backend[int]):
         return self.__dims
 
     def draw_tile(self, pos: IVec2) -> None:
-        self.__tilemap.draw_at(pos, self.__style, self.__pad)
+        self.__tilemap.draw_at(pos, self.__style, self.__pad.pad)
 
     def set_style(self, style: int) -> None:
         self.__style = style
 
     def present(self) -> None:
+        self.__screen.erase()
         self.__screen.refresh()
         y, x = self.__screen.getmaxyx()
         self.__layout.laid_out(IVec2(0, 0), IVec2(x, y))
@@ -128,9 +175,19 @@ class TTYBackend(Backend[int]):
         self.__screen.timeout(timeout_ms)
         try:
             key = self.__screen.getkey()
-            if key == "KEY_RESIZE":
-                self.__screen.erase()
-                return None
-            return KeyboardInput(key)
         except curses.error:
             return None
+        match key:
+            case "KEY_RESIZE":
+                pass
+            case "KEY_DOWN":
+                self.__pad.scroll(IVec2(0, 1))
+            case "KEY_UP":
+                self.__pad.scroll(IVec2(0, -1))
+            case "KEY_RIGHT":
+                self.__pad.scroll(IVec2(1, 0))
+            case "KEY_LEFT":
+                self.__pad.scroll(IVec2(-1, 0))
+            case _:
+                return KeyboardInput(key)
+        self.present()
index 0d053a19001a744b3b5b32da7389a4b8ae04cfd6..71551fbdb2def52411bb992a9c446441d69bfc33 100644 (file)
@@ -47,6 +47,12 @@ class IVec2[T = int]:
     def __mod__(self, other: "T | IVec2[T]") -> "IVec2[T]":
         return self.with_op(self.innertype().__mod__)(self, other)
 
+    def xy(self) -> tuple[T, T]:
+        return (self.x, self.y)
+
+    def yx(self) -> tuple[T, T]:
+        return (self.y, self.x)
+
 
 @dataclass
 class KeyboardInput: