From: Axy Date: Wed, 4 Mar 2026 01:21:25 +0000 (+0100) Subject: Scrollable pad X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=148c671545c0d18e83bc6c7d947769325843b018;p=axy%2Fft%2Fa-maze-ing.git Scrollable pad --- diff --git a/__main__.py b/__main__.py index 238d639..ce48924 100644 --- a/__main__.py +++ b/__main__.py @@ -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() diff --git a/amazeing/maze_display/TTYdisplay.py b/amazeing/maze_display/TTYdisplay.py index 0ec9461..fca9e2e 100644 --- a/amazeing/maze_display/TTYdisplay.py +++ b/amazeing/maze_display/TTYdisplay.py @@ -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() diff --git a/amazeing/maze_display/backend.py b/amazeing/maze_display/backend.py index 0d053a1..71551fb 100644 --- a/amazeing/maze_display/backend.py +++ b/amazeing/maze_display/backend.py @@ -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: