diff --git a/examples/advanced/connect4/connect4/__main__.py b/examples/advanced/connect4/connect4/__main__.py index 73563666..b36f62a8 100644 --- a/examples/advanced/connect4/connect4/__main__.py +++ b/examples/advanced/connect4/connect4/__main__.py @@ -22,13 +22,9 @@ def __init__(self): self._board = Board() h, w = self._board.size - super().__init__( - size=(h + 2, w), - pos_hint=(0.5, 0.5), - anchor="center", - ) + super().__init__(size=(h + 2, w), pos_hint=(0.5, 0.5)) - self._label = TextWidget(size=(1, 10), pos_hint=(None, 0.5), anchor="center") + self._label = TextWidget(size=(1, 10), pos_hint=(None, 0.5)) self.add_widgets(self._board, self._label) def on_add(self): diff --git a/examples/advanced/doom_fire.py b/examples/advanced/doom_fire.py index 612a32a2..1be78180 100644 --- a/examples/advanced/doom_fire.py +++ b/examples/advanced/doom_fire.py @@ -10,7 +10,7 @@ from nurses_2.colors import Color, ColorPair from nurses_2.widgets.graphic_widget import GraphicWidget from nurses_2.widgets.slider import Slider -from nurses_2.widgets.text_widget import Anchor, TextWidget +from nurses_2.widgets.text_widget import TextWidget from nurses_2.widgets.widget import Widget FIRE_PALETTE = np.array( @@ -133,7 +133,7 @@ async def on_start(self): strength_label = TextWidget( size=(1, 22), pos_hint=(None, 0.5), - anchor=Anchor.TOP_CENTER, + anchor="top", default_color_pair=SLIDER_DEFAULT, ) strength_label.add_str(f"Current Strength: {doomfire.fire_strength:2d}", (0, 1)) @@ -153,9 +153,7 @@ def slider_update(v): handle_color_pair=SLIDER_HANDLE, ) - slider_container = Widget( - size=(2, 38), pos_hint=(0, 0.5), anchor=Anchor.TOP_CENTER - ) + slider_container = Widget(size=(2, 38), pos_hint=(0, 0.5), anchor="top") slider_container.add_widgets(strength_label, slider) self.add_widgets(doomfire, slider_container) diff --git a/examples/advanced/fonts.py b/examples/advanced/fonts.py index f08df75e..bf389e67 100644 --- a/examples/advanced/fonts.py +++ b/examples/advanced/fonts.py @@ -131,7 +131,6 @@ async def on_start(self): size=button_canvas.shape, pos=(10, 0), pos_hint=(None, 0.5), - anchor="center", is_transparent=True, ) button.canvas["char"] = button_canvas @@ -140,9 +139,7 @@ async def on_start(self): banner = TextWidget(size=(h, w), is_transparent=True) banner.canvas["char"] = banner_canvas - bleed = Bleed( - banner, button, size=(h + 9, w), pos_hint=(0.5, 0.5), anchor="center" - ) + bleed = Bleed(banner, button, size=(h + 9, w), pos_hint=(0.5, 0.5)) self.add_widget(bleed) diff --git a/examples/advanced/hack/hack/__main__.py b/examples/advanced/hack/hack/__main__.py index 718e91cf..2d82f170 100644 --- a/examples/advanced/hack/hack/__main__.py +++ b/examples/advanced/hack/hack/__main__.py @@ -45,9 +45,7 @@ async def on_start(self): modal.memory = memory - terminal = Image( - path=TERMINAL, size=(36, 63), pos_hint=(0.5, 0.5), anchor="center" - ) + terminal = Image(path=TERMINAL, size=(36, 63), pos_hint=(0.5, 0.5)) container = BOLDCRT( size=(22, 53), pos=(5, 5), diff --git a/examples/advanced/hack/hack/modal.py b/examples/advanced/hack/hack/modal.py index a696f672..3aa94f0f 100644 --- a/examples/advanced/hack/hack/modal.py +++ b/examples/advanced/hack/hack/modal.py @@ -30,7 +30,6 @@ def __init__(self, **kwargs): self.message_box = TextWidget( size=(6, 20), pos_hint=(0.5, 0.5), - anchor="center", default_color_pair=DEFAULT_COLOR_PAIR, ) self.message_box.add_border("heavy") diff --git a/examples/advanced/io_events.py b/examples/advanced/io_events.py index 2f78fc2c..f271b2d1 100644 --- a/examples/advanced/io_events.py +++ b/examples/advanced/io_events.py @@ -312,14 +312,14 @@ def on_mouse(self, mouse_event): class InputApp(App): async def on_start(self): - keyboard = KeyboardWidget(pos_hint=(0.5, 0), anchor="left_center") - mouse = MouseWidget(pos_hint=(0.5, 1.0), anchor="right_center") + keyboard = KeyboardWidget(pos_hint=(0.5, 0), anchor="left") + mouse = MouseWidget(pos_hint=(0.5, 1.0), anchor="right") container_size = ( max(keyboard.height, mouse.height), keyboard.width + mouse.width + 2, ) - container = Widget(size=container_size, pos_hint=(0.5, 0.5), anchor="center") + container = Widget(size=container_size, pos_hint=(0.5, 0.5)) container.add_widgets(keyboard, mouse) self.add_widget(container) diff --git a/examples/advanced/minesweeper/minesweeper/minefield.py b/examples/advanced/minesweeper/minesweeper/minefield.py index 630cb643..aa9aaec0 100644 --- a/examples/advanced/minesweeper/minesweeper/minefield.py +++ b/examples/advanced/minesweeper/minesweeper/minefield.py @@ -5,7 +5,7 @@ from nurses_2.io import MouseButton, MouseEventType -from .colors import * +from .colors import FLAG_COLOR, HIDDEN, HIDDEN_REVERSED from .grid import Grid from .unicode_chars import FLAG @@ -232,3 +232,12 @@ def render(self, canvas_view, colors_view, source: tuple[slice, slice]): colors_view[visible] = self.colors[source][visible] self.render_children(source, canvas_view, colors_view) + + if self.hidden_cells.sum() == self.nmines: + self._game_over(win=True) + return + + def _game_over(self, win: bool): + self.hidden[:] = 0 + self._is_gameover = True + self.parent.game_over(win=win) diff --git a/examples/advanced/minesweeper/minesweeper/minesweeper.py b/examples/advanced/minesweeper/minesweeper/minesweeper.py index bfbe5cfa..40041617 100644 --- a/examples/advanced/minesweeper/minesweeper/minesweeper.py +++ b/examples/advanced/minesweeper/minesweeper/minesweeper.py @@ -4,14 +4,23 @@ import numpy as np from nurses_2.widgets.behaviors.button_behavior import ButtonBehavior -from nurses_2.widgets.text_widget import Anchor, Point, TextWidget +from nurses_2.widgets.text_widget import Point, TextWidget from nurses_2.widgets.widget import Widget from .colors import DATA_BAR, FLAG_COLOR from .count import Count from .grid import Grid from .minefield import Minefield -from .unicode_chars import * +from .unicode_chars import ( + BAD_FLAG, + BOMB, + COOL, + EXPLODED, + FLAG, + HAPPY, + KNOCKED_OUT, + SURPRISED, +) SIZE = 16, 30 NMINES = 99 @@ -47,7 +56,7 @@ def __init__(self, pos=Point(0, 0), **kwargs): self.timer = TextWidget( size=(1, 20), - anchor=Anchor.TOP_RIGHT, + anchor="top-right", pos_hint=(None, 0.95), default_color_pair=DATA_BAR, ) @@ -64,7 +73,6 @@ def __init__(self, pos=Point(0, 0), **kwargs): self.reset_button = ResetButton( size=(1, 2), default_color_pair=DATA_BAR, - anchor=Anchor.CENTER, pos_hint=(None, 0.5), ) diff --git a/examples/advanced/palettes.py b/examples/advanced/palettes.py index cb07c226..f53f567c 100644 --- a/examples/advanced/palettes.py +++ b/examples/advanced/palettes.py @@ -124,7 +124,7 @@ def update_palette(): hue_selector.callback = slope_selector.callback = update_palette - container = Widget(size=(H + 2, W), pos_hint=(0.5, 0.5), anchor="center") + container = Widget(size=(H + 2, W), pos_hint=(0.5, 0.5)) container.add_widgets(hue_selector, slope_selector, palette) self.add_widget(container) diff --git a/examples/advanced/rigid_body_physics.py b/examples/advanced/rigid_body_physics.py index 9abca8b0..daa1c55c 100644 --- a/examples/advanced/rigid_body_physics.py +++ b/examples/advanced/rigid_body_physics.py @@ -4,9 +4,9 @@ Requires `pymunk` """ import asyncio -from enum import Enum from math import ceil, degrees from pathlib import Path +from typing import Literal import cv2 import numpy as np @@ -23,13 +23,6 @@ ) from nurses_2.widgets.image import Image - -class RenderMode(str, Enum): - OUTLINE = "outline" - FILL = "fill" - SPRITE = "sprite" - - BOX_SIZE = W, H = Vec2d(9, 9) BOX_MASS = 0.1 BALL_MASS = 100 @@ -54,14 +47,14 @@ def __init__( self, space: pymunk.Space, dt: float = 0.01, - render_mode: RenderMode = RenderMode.SPRITE, + render_mode: Literal["outline", "fill", "sprite"] = "sprite", shape_color: AColor = AWHITE, **kwargs, ): super().__init__(**kwargs) self.space = space self.dt = dt - self.render_mode = RenderMode(render_mode) + self.render_mode = render_mode self.shape_color = shape_color def stop_simulation(self): @@ -91,7 +84,7 @@ def _draw_space(self): for shape in self.space.shapes: if isinstance(shape, pymunk.shapes.Segment): - if self.render_mode is not RenderMode.SPRITE: + if self.render_mode != "sprite": a = shape.a.rotated(shape.body.angle) + shape.body.position b = shape.b.rotated(shape.body.angle) + shape.body.position cv2.line( @@ -101,7 +94,7 @@ def _draw_space(self): self.shape_color, ) elif isinstance(shape, pymunk.shapes.Poly): - if self.render_mode is RenderMode.SPRITE: + if self.render_mode == "sprite": angle = degrees(shape.body.angle) rot = cv2.getRotationMatrix2D(BOX_CENTER, angle, 1) box = cv2.boxPoints((BOX_CENTER, BOX_SIZE, angle)) @@ -127,7 +120,7 @@ def _draw_space(self): for vertex in shape.get_vertices() ] ) - if self.render_mode is RenderMode.FILL: + if self.render_mode == "fill": cv2.fillPoly(self.texture, [vertices], color=self.shape_color) else: cv2.polylines( @@ -137,7 +130,7 @@ def _draw_space(self): color=self.shape_color, ) elif isinstance(shape, pymunk.shapes.Circle): - if self.render_mode is RenderMode.SPRITE: + if self.render_mode == "sprite": angle == degrees(shape.body.angle) rot = cv2.getRotationMatrix2D(BALL_CENTER, angle, 1) src = cv2.warpAffine(BALL, rot, BALL_SIZE) @@ -149,7 +142,7 @@ def _draw_space(self): pos = shape.body.position center = to_tex_coords(pos) circle_edge = pos + Vec2d(shape.radius, 0).rotated(shape.body.angle) - if self.render_mode is RenderMode.FILL: + if self.render_mode == "fill": cv2.circle( self.texture, center, diff --git a/examples/advanced/sandbox/sandbox/sandbox.py b/examples/advanced/sandbox/sandbox/sandbox.py index 7d48dd6a..86162c1d 100644 --- a/examples/advanced/sandbox/sandbox/sandbox.py +++ b/examples/advanced/sandbox/sandbox/sandbox.py @@ -4,7 +4,7 @@ from nurses_2.colors import ABLACK, ColorPair from nurses_2.io import MouseButton -from nurses_2.widgets.graphic_widget import Anchor, GraphicWidget, Size +from nurses_2.widgets.graphic_widget import GraphicWidget, Size from nurses_2.widgets.text_widget import TextWidget from .element_buttons import MENU_BACKGROUND_COLOR, ButtonContainer @@ -25,9 +25,7 @@ class Sandbox(GraphicWidget): """ def __init__(self, size: Size): - super().__init__( - size=size, anchor=Anchor.CENTER, pos_hint=(0.5, 0.5), default_color=ABLACK - ) + super().__init__(size=size, pos_hint=(0.5, 0.5), default_color=ABLACK) def on_add(self): super().on_add() @@ -40,7 +38,6 @@ def on_add(self): self.display = TextWidget( size=(1, 9), pos=(1, 0), - anchor=Anchor.CENTER, pos_hint=(None, 0.5), default_color_pair=ColorPair.from_colors(Air.COLOR, MENU_BACKGROUND_COLOR), ) diff --git a/examples/advanced/shadow_casting.py b/examples/advanced/shadow_casting.py index f011b49e..e6b760fd 100644 --- a/examples/advanced/shadow_casting.py +++ b/examples/advanced/shadow_casting.py @@ -62,7 +62,6 @@ async def on_start(self): caster = ShadowCaster( size=(17, 34), - anchor="center", map=map_, camera=Camera((0, 0), (34, 34)), tile_colors=[AWHITE, ACYAN, AMAGENTA], @@ -196,7 +195,7 @@ def callback(state): ) grid_layout.size = grid_layout.minimum_grid_size - container = Widget(pos_hint=(0.5, 0.5), anchor="center", size=(17, 58)) + container = Widget(pos_hint=(0.5, 0.5), size=(17, 58)) container.add_widgets(caster, grid_layout) self.add_widget(container) diff --git a/examples/advanced/snake.py b/examples/advanced/snake.py index 92a562fa..3fa7ed57 100644 --- a/examples/advanced/snake.py +++ b/examples/advanced/snake.py @@ -122,14 +122,9 @@ async def _update(self): class SnakeApp(App): async def on_start(self): - kwargs = dict( - size=(HEIGHT // 2, WIDTH), - pos_hint=(0.5, 0.5), - anchor="center", - ) - + kwargs = dict(size=(HEIGHT // 2, WIDTH), pos_hint=(0.5, 0.5)) background = Animation(path=SPINNER, alpha=0.5, **kwargs) - # background.play() + background.play() snake = Snake(**kwargs) self.add_widgets(background, snake) diff --git a/examples/advanced/sph/sph/__main__.py b/examples/advanced/sph/sph/__main__.py index ded02e6a..c461f9dc 100644 --- a/examples/advanced/sph/sph/__main__.py +++ b/examples/advanced/sph/sph/__main__.py @@ -5,7 +5,7 @@ from nurses_2.app import App from nurses_2.colors import BLACK, WHITE_ON_BLACK, Color, ColorPair from nurses_2.io import MouseButton -from nurses_2.widgets.graphic_widget import Anchor, GraphicWidget +from nurses_2.widgets.graphic_widget import GraphicWidget from nurses_2.widgets.slider import Slider from nurses_2.widgets.text_widget import TextWidget @@ -95,7 +95,6 @@ async def on_start(self): container = TextWidget( size=(height, width), pos_hint=(0.5, 0.5), - anchor=Anchor.CENTER, default_color_pair=WHITE_ON_BLACK, ) diff --git a/examples/advanced/tetris/tetris/__main__.py b/examples/advanced/tetris/tetris/__main__.py index 6ee55277..34b7a31c 100644 --- a/examples/advanced/tetris/tetris/__main__.py +++ b/examples/advanced/tetris/tetris/__main__.py @@ -1,12 +1,11 @@ from nurses_2.app import App -from nurses_2.widgets.widget_data_structures import Anchor from .tetris import Tetris class TetrisApp(App): async def on_start(self): - tetris = Tetris(pos_hint=(0.5, 0.5), anchor=Anchor.CENTER) + tetris = Tetris(pos_hint=(0.5, 0.5)) self.add_widget(tetris) diff --git a/examples/advanced/tetris/tetris/modal_screen.py b/examples/advanced/tetris/tetris/modal_screen.py index 1f3c0547..9a602677 100644 --- a/examples/advanced/tetris/tetris/modal_screen.py +++ b/examples/advanced/tetris/tetris/modal_screen.py @@ -3,7 +3,7 @@ import numpy as np from nurses_2.colors import BLACK, Color, ColorPair, gradient -from nurses_2.widgets.text_widget import Anchor, TextWidget +from nurses_2.widgets.text_widget import TextWidget LIGHT_PURPLE = Color.from_hex("8d46dd") DARK_PURPLE = Color.from_hex("190c54") @@ -78,14 +78,12 @@ class ModalScreen(TextWidget): def __init__( self, - anchor=Anchor.CENTER, pos_hint=(0.5, 0.5), is_enabled=False, **kwargs, ): super().__init__( size=(10, 70), - anchor=anchor, pos_hint=pos_hint, is_enabled=is_enabled, **kwargs, diff --git a/examples/advanced/tetris/tetris/tetris.py b/examples/advanced/tetris/tetris/tetris.py index 4bad8602..191ca392 100644 --- a/examples/advanced/tetris/tetris/tetris.py +++ b/examples/advanced/tetris/tetris/tetris.py @@ -163,10 +163,10 @@ def __init__(self, matrix_size=(22, 10), arika=True, **kwargs): for widget in self.walk(): setup_background(widget) - self.held_piece = Piece(pos_hint=(0.5, 0.5), anchor="center", is_enabled=False) + self.held_piece = Piece(pos_hint=(0.5, 0.5), is_enabled=False) held_space.add_widget(self.held_piece) - self.next_piece = Piece(pos_hint=(0.5, 0.5), anchor="center", is_enabled=False) + self.next_piece = Piece(pos_hint=(0.5, 0.5), is_enabled=False) next_space.add_widget(self.next_piece) self.matrix = np.zeros(matrix_size, dtype=np.bool8) diff --git a/examples/basic/animation.py b/examples/basic/animation.py index 5a4be38a..fd3bf701 100644 --- a/examples/basic/animation.py +++ b/examples/basic/animation.py @@ -1,8 +1,7 @@ from pathlib import Path from nurses_2.app import App -from nurses_2.widgets.animation import Animation, Interpolation -from nurses_2.widgets.widget_data_structures import Anchor +from nurses_2.widgets.animation import Animation ASSETS = Path(__file__).parent.parent / "assets" PATH_TO_FRAMES = ASSETS / "caveman" @@ -12,10 +11,9 @@ class MyApp(App): async def on_start(self): animation = Animation( size_hint=(0.5, 0.5), - anchor=Anchor.CENTER, pos_hint=(0.5, 0.5), path=PATH_TO_FRAMES, - interpolation=Interpolation.NEAREST, + interpolation="nearest", ) self.add_widget(animation) diff --git a/examples/basic/borders.py b/examples/basic/borders.py index 4cf6b356..e0e90ab2 100644 --- a/examples/basic/borders.py +++ b/examples/basic/borders.py @@ -4,13 +4,13 @@ border_colors = [ ColorPair.from_colors(fg, BLACK) - for fg in rainbow_gradient(len(Border), color_type=Color) + for fg in rainbow_gradient(len(Border.__args__), color_type=Color) ] class BordersApp(App): async def on_start(self): - for i, border in enumerate(Border): + for i, border in enumerate(Border.__args__): widget = TextWidget(size=(3, 17), pos=(i * 3, 0)) widget.add_border(border, color_pair=border_colors[i]) widget.add_str(f"{border:^15}", pos=(1, 1), italic=True) diff --git a/examples/basic/buttons.py b/examples/basic/buttons.py index e3e04a7d..1c0e6ea5 100644 --- a/examples/basic/buttons.py +++ b/examples/basic/buttons.py @@ -4,7 +4,7 @@ from nurses_2.app import App from nurses_2.widgets.button import Button from nurses_2.widgets.flat_toggle import FlatToggle -from nurses_2.widgets.grid_layout import GridLayout, Orientation +from nurses_2.widgets.grid_layout import GridLayout from nurses_2.widgets.text_widget import TextWidget from nurses_2.widgets.toggle_button import ToggleButton, ToggleState @@ -30,7 +30,7 @@ def callback(state): grid_rows=5, grid_columns=3, pos=(2, 0), - orientation=Orientation.TB_LR, + orientation="tb-lr", padding_left=1, padding_right=1, padding_top=1, @@ -71,7 +71,7 @@ def callback(state): grid_rows=2, grid_columns=5, pos=(grid_layout.bottom, 7), - orientation=Orientation.LR_TB, + orientation="lr-tb", horizontal_spacing=1, ) diff --git a/examples/basic/easings.py b/examples/basic/easings.py index 66bb500e..2bc6a034 100644 --- a/examples/basic/easings.py +++ b/examples/basic/easings.py @@ -3,7 +3,7 @@ from nurses_2.app import App from nurses_2.widgets.image import Image -from nurses_2.widgets.text_widget import Anchor, TextWidget +from nurses_2.widgets.text_widget import TextWidget from nurses_2.widgets.widget_data_structures import Easing ASSETS = Path(__file__).parent.parent / "assets" @@ -22,11 +22,11 @@ async def on_start(self): pos_hint=next(POS_HINTS), ) - label = TextWidget(size=(1, 30), pos_hint=(None, 0.5), anchor=Anchor.TOP_CENTER) + label = TextWidget(size=(1, 30), pos_hint=(None, 0.5), anchor="top") self.add_widgets(logo, label) - for easing in Easing: + for easing in Easing.__args__: label.add_str(f"{easing:^30}") await logo.tween( diff --git a/examples/basic/line_plot.py b/examples/basic/line_plot.py index c8214bd8..6d1040be 100644 --- a/examples/basic/line_plot.py +++ b/examples/basic/line_plot.py @@ -46,7 +46,7 @@ def set_braille_mode(toggle_state): ) container = Widget( - size=(2, BUTTON_WIDTH), pos_hint=(None, 1.0), anchor="top_right" + size=(2, BUTTON_WIDTH), pos_hint=(None, 1.0), anchor="top-right" ) container.add_widgets(box_button, braille_button) self.add_widgets(plot, container) diff --git a/examples/basic/scroll_view.py b/examples/basic/scroll_view.py index bbbf8e65..c8510ea6 100644 --- a/examples/basic/scroll_view.py +++ b/examples/basic/scroll_view.py @@ -4,7 +4,7 @@ from nurses_2.app import App from nurses_2.colors import BLUE, GREEN, RED, WHITE, ColorPair, gradient from nurses_2.widgets.scroll_view import ScrollView -from nurses_2.widgets.text_widget import Anchor, Size, TextWidget +from nurses_2.widgets.text_widget import Size, TextWidget N = 20 # Number of coordinate pairs on each line. BIG_WIDGET_SIZE = Size(50, 8 * N + N - 1) @@ -27,9 +27,7 @@ async def on_start(self): LEFT_GRADIENT[y], RIGHT_GRADIENT[y], BIG_WIDGET_SIZE.columns ) - scroll_view = ScrollView( - size=(10, 30), anchor=Anchor.CENTER, pos_hint=(0.5, 0.5) - ) + scroll_view = ScrollView(size=(10, 30), pos_hint=(0.5, 0.5)) scroll_view.view = big_widget self.add_widget(scroll_view) diff --git a/examples/basic/spinners.py b/examples/basic/spinners.py index 3e02b469..eb314581 100644 --- a/examples/basic/spinners.py +++ b/examples/basic/spinners.py @@ -26,7 +26,7 @@ async def on_start(self): ) for name, frames in SPINNERS.items(): - label = TextWidget(pos_hint=(0.5, None), anchor="left_center") + label = TextWidget(pos_hint=(0.5, None), anchor="left") label.set_text(f"{name}: ") animation = TextAnimation(pos=(0, label.right), frames=frames) diff --git a/examples/basic/tabbed_widget.py b/examples/basic/tabbed_widget.py index 36f83559..f0263de8 100644 --- a/examples/basic/tabbed_widget.py +++ b/examples/basic/tabbed_widget.py @@ -4,7 +4,7 @@ from nurses_2.app import App from nurses_2.colors import DEFAULT_COLOR_THEME -from nurses_2.widgets.animation import Animation, Interpolation +from nurses_2.widgets.animation import Animation from nurses_2.widgets.color_picker import ColorPicker from nurses_2.widgets.file_chooser import FileChooser from nurses_2.widgets.line_plot import LinePlot @@ -23,7 +23,7 @@ async def on_start(self): tabbed = TabbedWidget(size_hint=(1.0, 1.0)) animation = Animation( - path=CAVEMAN_PATH, interpolation=Interpolation.NEAREST, size_hint=(1.0, 1.0) + path=CAVEMAN_PATH, interpolation="nearest", size_hint=(1.0, 1.0) ) animation.play() tabbed.add_tab("Animation", animation) @@ -34,16 +34,12 @@ async def on_start(self): tabbed.add_tab( "Line Plot", LinePlot( - XS, - YS_1, - XS, - YS_2, - XS, - YS_3, - xlabel="X Values", - ylabel="Y Values", + xs=[XS, XS, XS], + ys=[YS_1, YS_2, YS_3], + x_label="X Values", + y_label="Y Values", legend_labels=("Before", "During", "After"), - background_color_pair=DEFAULT_COLOR_THEME.primary, + plot_color_pair=DEFAULT_COLOR_THEME.primary, size_hint=(1.0, 1.0), ), ) diff --git a/examples/basic/text_input.py b/examples/basic/text_input.py index b618e43b..ec9ee932 100644 --- a/examples/basic/text_input.py +++ b/examples/basic/text_input.py @@ -69,7 +69,7 @@ def enter_callback(textbox): pos=(1, 0), size=(3, 35), pos_hint=(None, 0.5), - anchor="top_center", + anchor="top", default_color_pair=DEFAULT_COLOR_THEME.textbox_primary, ) border.add_border() diff --git a/examples/basic/windows.py b/examples/basic/windows.py index b0591a39..14e83071 100644 --- a/examples/basic/windows.py +++ b/examples/basic/windows.py @@ -4,7 +4,7 @@ from nurses_2.app import App from nurses_2.colors import DEFAULT_COLOR_THEME -from nurses_2.widgets.animation import Animation, Interpolation +from nurses_2.widgets.animation import Animation from nurses_2.widgets.color_picker import ColorPicker from nurses_2.widgets.file_chooser import FileChooser from nurses_2.widgets.line_plot import LinePlot @@ -24,7 +24,7 @@ class MyApp(App): async def on_start(self): window_kwargs = dict(size=(25, 50), border_alpha=0.7, alpha=0.7) - animation = Animation(path=CAVEMAN_PATH, interpolation=Interpolation.NEAREST) + animation = Animation(path=CAVEMAN_PATH, interpolation="nearest") window_1 = Window(title=CAVEMAN_PATH.name, **window_kwargs) window_1.view = animation @@ -36,16 +36,12 @@ async def on_start(self): window_4 = Window(title="Line Plot", **window_kwargs) window_4.view = LinePlot( - XS, - YS_1, - XS, - YS_2, - XS, - YS_3, - xlabel="X Values", - ylabel="Y Values", + xs=[XS, XS, XS], + ys=[YS_1, YS_2, YS_3], + x_label="X Values", + y_label="Y Values", legend_labels=("Before", "During", "After"), - background_color_pair=DEFAULT_COLOR_THEME.primary, + plot_color_pair=DEFAULT_COLOR_THEME.primary, ) self.add_widgets(window_1, window_2, window_3, window_4) diff --git a/nurses_2/__init__.py b/nurses_2/__init__.py index 7ae0f604..db491179 100644 --- a/nurses_2/__init__.py +++ b/nurses_2/__init__.py @@ -4,4 +4,4 @@ A widgetful and async-centric terminal graphics library. """ -__version__ = "0.23.0" +__version__ = "0.24.0" diff --git a/nurses_2/widgets/animation.py b/nurses_2/widgets/animation.py index 3b38476c..8aa67268 100644 --- a/nurses_2/widgets/animation.py +++ b/nurses_2/widgets/animation.py @@ -6,13 +6,14 @@ from pathlib import Path import numpy as np +from numpy.typing import NDArray from ..clamp import clamp from .graphic_widget_data_structures import Interpolation from .image import Image from .widget import Widget, subscribable -__all__ = "Animation", "Interpolation" +__all__ = ("Animation",) def _check_frame_durations(frames, frame_durations): @@ -47,7 +48,7 @@ class Animation(Widget): If true, play animation in reverse. alpha : float, default: 1.0 Transparency of the animation. - interpolation : Interpolation, default: Interpolation.Linear + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -71,7 +72,7 @@ class Animation(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: True If false, :attr:`alpha` and alpha channels are ignored. @@ -178,7 +179,7 @@ class Animation(Widget): stop: Stop the animation and reset current frame. from_textures: - Create an :class:`Animation` from an iterable of uint8 rgba ndarray. + Create an :class:`Animation` from an iterable of uint8 RGBA numpy array. from_images: Create an :class:`Animation` from an iterable of :class:`Image`. on_size: @@ -233,7 +234,7 @@ def __init__( loop: bool = True, reverse: bool = False, alpha: float = 1.0, - interpolation: Interpolation = Interpolation.LINEAR, + interpolation: Interpolation = "linear", is_transparent: bool = True, **kwargs, ): @@ -360,13 +361,13 @@ def render(self, canvas_view, colors_view, source: tuple[slice, slice]): @classmethod def from_textures( cls, - textures: Iterable[np.ndarray], + textures: Iterable[NDArray[np.uint8]], *, frame_durations: float | int | Sequence[float | int] = 1 / 12, **kwargs, ) -> "Animation": """ - Create an :class:`Animation` from an iterable of uint8 rgba ndarray. + Create an :class:`Animation` from an iterable of uint8 RGBA numpy array. """ animation = cls(**kwargs) animation.frames = [ diff --git a/nurses_2/widgets/box_image.py b/nurses_2/widgets/box_image.py index a78fea04..3e4310d8 100644 --- a/nurses_2/widgets/box_image.py +++ b/nurses_2/widgets/box_image.py @@ -45,7 +45,7 @@ class BoxImage(TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. diff --git a/nurses_2/widgets/braille_image.py b/nurses_2/widgets/braille_image.py index b5e0636e..9b770355 100644 --- a/nurses_2/widgets/braille_image.py +++ b/nurses_2/widgets/braille_image.py @@ -45,7 +45,7 @@ class BrailleImage(TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. diff --git a/nurses_2/widgets/braille_video_player.py b/nurses_2/widgets/braille_video_player.py index 76c97062..d0fac20b 100644 --- a/nurses_2/widgets/braille_video_player.py +++ b/nurses_2/widgets/braille_video_player.py @@ -64,7 +64,7 @@ class BrailleVideoPlayer(TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. diff --git a/nurses_2/widgets/button.py b/nurses_2/widgets/button.py index 98966a01..ee66c0ea 100644 --- a/nurses_2/widgets/button.py +++ b/nurses_2/widgets/button.py @@ -7,7 +7,7 @@ from .behaviors.button_behavior import ButtonBehavior, ButtonState from .behaviors.themable import Themable -from .text_widget import Anchor, TextWidget +from .text_widget import TextWidget from .widget import Widget @@ -45,7 +45,7 @@ class Button(Themable, ButtonBehavior, Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -203,7 +203,7 @@ def __init__( callback: Callable[[], None] = lambda: None, **kwargs, ): - self._label_widget = TextWidget(pos_hint=(0.5, 0.5), anchor=Anchor.CENTER) + self._label_widget = TextWidget(pos_hint=(0.5, 0.5)) super().__init__(background_char=background_char, **kwargs) diff --git a/nurses_2/widgets/color_picker.py b/nurses_2/widgets/color_picker.py index 3d5af5cd..b988342f 100644 --- a/nurses_2/widgets/color_picker.py +++ b/nurses_2/widgets/color_picker.py @@ -172,7 +172,7 @@ class ColorPicker(Themable, Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/data_table.py b/nurses_2/widgets/data_table.py index b6f4116e..ac2cfec7 100644 --- a/nurses_2/widgets/data_table.py +++ b/nurses_2/widgets/data_table.py @@ -5,39 +5,19 @@ from dataclasses import dataclass, replace from enum import Enum from itertools import count, islice -from typing import Protocol, TypeVar +from typing import Literal, Protocol, TypeVar from wcwidth import wcswidth from ..io import MouseEvent from .behaviors.button_behavior import ButtonBehavior from .behaviors.themable import Themable -from .grid_layout import GridLayout, Orientation +from .grid_layout import GridLayout from .scroll_view import ScrollView from .text_widget import Point, TextWidget from .text_widget_data_structures import add_text -__all__ = "SelectItems", "Alignment", "ColumnStyle", "DataTable" - - -class SelectItems(str, Enum): - """ - Determines whether rows, columns or cells are selected in a data table. - """ - - CELL = "cell" - ROW = "row" - COLUMN = "column" - - -class Alignment(str, Enum): - """ - Alignments of a column in a data table. - """ - - CENTER = "center" - LEFT = "left" - RIGHT = "right" +__all__ = "ColumnStyle", "DataTable" class SupportsLessThan(Protocol): @@ -63,8 +43,8 @@ class ColumnStyle: render : Callable[[T], str] | None, default: None A callable that renders column data into a string. Uses the built-in `str` by default. - alignment : Alignment, default: Alignment.LEFT - Alignment of the column. One of `"left"`, `"right"`, `"center"`. + alignment : Literal["center", "left", "right"], default: "left" + Alignment of the column. padding : int, default: 1 Left and right padding of column. min_width : int, default: 0 @@ -76,7 +56,7 @@ class ColumnStyle: ---------- render : Callable[[T], str] A callable that renders column data into a string. - alignment : Alignment + alignment : Literal["center", "left", "right"] Alignment of the column. padding : int Left and right padding of column. @@ -91,9 +71,9 @@ class ColumnStyle: A callable that renders column data into a string. Uses the built-in `str` by default. """ - alignment: Alignment = Alignment.LEFT + alignment: Literal["center", "left", "right"] = "left" """ - Alignment of the column. One of `"left"`, `"right"`, `"center"`. + Alignment of the column. """ padding: int = 1 """ @@ -127,7 +107,11 @@ class _SortState(str, Enum): """Spaces between column label text and sort indicator.""" _SORT_INDICATOR_WIDTH = wcswidth(_SortState.NOT_SORTED.value) """Character width of sort indicator. (Indicator values should be same width.)""" -_ALIGN_FORMATTER = {Alignment.CENTER: "^", Alignment.LEFT: "<", Alignment.RIGHT: ">"} +_ALIGN_FORMATTER = { + "center": "^", + "left": "<", + "right": ">", +} """Convert an alignment to f-string format specification.""" @@ -326,7 +310,7 @@ class DataTable(Themable, ScrollView): control over column styling use :meth:`add_column`. default_style : ColumnStyle | None, default: None Default style for new columns. - select_items : SelectItems, default: SelectItems.Row + select_items : Literal["cell", "row", "column"], default: "row" Determines which items are selected when data table is clicked. zebra_stripes : bool, default: True Whether alternate rows are colored differently. @@ -376,7 +360,7 @@ class DataTable(Themable, ScrollView): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -395,7 +379,7 @@ class DataTable(Themable, ScrollView): ---------- default_style : ColumnStyle Default style for new columns. - select_items : SelectItems + select_items : Literal["cell", "row", "column"] Which items are selected when data table is clicked. zebra_stripes : bool Whether alternate rows are colored differently. @@ -576,7 +560,7 @@ def __init__( self, data: dict[str, Sequence[T]] | None = None, default_style: ColumnStyle | None = None, - select_items: SelectItems = SelectItems.ROW, + select_items: Literal["cell", "row", "column"] = "row", zebra_stripes: bool = True, allow_sorting: bool = True, **kwargs, @@ -587,7 +571,7 @@ def __init__( self._column_styles: dict[int, ColumnStyle] = {} """Column id to column style.""" self._column_labels = GridLayout( - grid_rows=1, grid_columns=0, orientation=Orientation.LR_TB + grid_rows=1, grid_columns=0, orientation="lr-tb" ) """Grid layout containing column label cells.""" self._rows: dict[int, GridLayout] = {} @@ -598,9 +582,7 @@ def __init__( """ self._hover_row_id = -1 """Row id of row that mouse is hovering or -1 if no columns are hovered.""" - self._table = GridLayout( - grid_rows=1, grid_columns=1, orientation=Orientation.TB_LR - ) + self._table = GridLayout(grid_rows=1, grid_columns=1, orientation="tb-lr") """Grid layout of column label and row grid layouts.""" self._table.add_widget(self._column_labels) self.view = self._table @@ -619,15 +601,15 @@ def __init__( self.add_column(label, data=column_data) @property - def select_items(self) -> SelectItems: + def select_items(self) -> Literal["cell", "row", "column"]: """ Determines which items are selected when data table is clicked. """ return self._select_items @select_items.setter - def select_items(self, select_items: SelectItems): - select_items = SelectItems(select_items) + def select_items(self, select_items: Literal["cell", "row", "column"]): + select_items = select_items self._select_items = select_items for row in self._iter_rows(): for cell in row.children: @@ -716,7 +698,7 @@ def _update_hover(self, column_id: int = -1, row_id: int = -1): cell: _DataCell match self.select_items: - case SelectItems.ROW: + case "row": if self._hover_row_id != row_id: if self._hover_row_id != -1: for cell in self._rows[self._hover_row_id].children: @@ -724,7 +706,7 @@ def _update_hover(self, column_id: int = -1, row_id: int = -1): if row_id != -1: for cell in self._rows[row_id].children: self._paint_cell_hover(cell) - case SelectItems.COLUMN: + case "column": if self._hover_column_id != column_id: if self._hover_column_id != -1: old_column_index = self._column_ids.index(self._hover_column_id) @@ -734,7 +716,7 @@ def _update_hover(self, column_id: int = -1, row_id: int = -1): new_column_index = self._column_ids.index(column_id) for row in self._rows.values(): self._paint_cell_hover(row.children[new_column_index]) - case SelectItems.CELL: + case "cell": if self._hover_column_id != column_id or self._hover_row_id != row_id: if self._hover_column_id != -1 and self._hover_row_id != -1: old_column_index = self._column_ids.index(self._hover_column_id) @@ -754,17 +736,17 @@ def _on_release(self): cell: _DataCell match self.select_items: - case SelectItems.ROW: + case "row": for cell in self._rows[self._hover_row_id].children: cell.selected = not cell.selected self._paint_cell_hover(cell) - case SelectItems.COLUMN: + case "column": column_index = self._column_ids.index(self._hover_column_id) for row in self._rows.values(): cell = row.children[column_index] cell.selected = not cell.selected self._paint_cell_hover(cell) - case SelectItems.CELL: + case "cell": column_index = self._column_ids.index(self._hover_column_id) cell = self._rows[self._hover_row_id].children[column_index] cell.selected = not cell.selected @@ -858,9 +840,7 @@ def add_column( self._table.grid_rows += len(data) for item in data: row_id = next(self._IDS) - row = GridLayout( - grid_rows=1, grid_columns=1, orientation=Orientation.LR_BT - ) + row = GridLayout(grid_rows=1, grid_columns=1, orientation="lr-bt") row.add_widget( _DataCell( data_table=self, column_id=column_id, data=item, row_id=row_id @@ -905,7 +885,7 @@ def add_row(self, data: Sequence[SupportsLessThan]) -> int: row_id = next(self._IDS) row_layout = GridLayout( - grid_rows=1, grid_columns=len(data), orientation=Orientation.LR_BT + grid_rows=1, grid_columns=len(data), orientation="lr-bt" ) row_layout.add_widgets( _DataCell(data_table=self, column_id=column_id, data=item, row_id=row_id) diff --git a/nurses_2/widgets/digital_display.py b/nurses_2/widgets/digital_display.py index 0c8eb957..2570bddb 100644 --- a/nurses_2/widgets/digital_display.py +++ b/nurses_2/widgets/digital_display.py @@ -172,7 +172,7 @@ class DigitalDisplay(TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. diff --git a/nurses_2/widgets/file_chooser.py b/nurses_2/widgets/file_chooser.py index f32f1d7b..00a86f02 100644 --- a/nurses_2/widgets/file_chooser.py +++ b/nurses_2/widgets/file_chooser.py @@ -229,7 +229,7 @@ class FileChooser(Themable, ScrollView): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/flat_toggle.py b/nurses_2/widgets/flat_toggle.py index acf07e77..f47901b0 100644 --- a/nurses_2/widgets/flat_toggle.py +++ b/nurses_2/widgets/flat_toggle.py @@ -22,7 +22,6 @@ def __init__( super().__init__( size=(3, 4), pos_hint=(0.5, 0.5), - anchor="center", group=group, allow_no_selection=allow_no_selection, toggle_state=toggle_state, @@ -118,7 +117,7 @@ class FlatToggle(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/graphic_field.py b/nurses_2/widgets/graphic_field.py index 9e62294b..22d4fbd2 100644 --- a/nurses_2/widgets/graphic_field.py +++ b/nurses_2/widgets/graphic_field.py @@ -55,7 +55,7 @@ class GraphicParticleField(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: True If false, :attr:`particle_alphas` and alpha channels are ignored. diff --git a/nurses_2/widgets/graphic_widget.py b/nurses_2/widgets/graphic_widget.py index fc705825..4ee0ae66 100644 --- a/nurses_2/widgets/graphic_widget.py +++ b/nurses_2/widgets/graphic_widget.py @@ -42,7 +42,7 @@ class GraphicWidget(Widget): alpha : float, default: 1.0 If widget is transparent, the alpha channel of the underlying texture will be multiplied by this value. (0 <= alpha <= 1.0) - interpolation : Interpolation, default: Interpolation.LINEAR + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -66,7 +66,7 @@ class GraphicWidget(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If false, :attr:`alpha` and alpha channels are ignored. @@ -83,7 +83,7 @@ class GraphicWidget(Widget): Attributes ---------- - texture : numpy.ndarray + texture : NDArray[np.uint8] uint8 RGBA color array. default_color : AColor Default texture color. @@ -213,7 +213,7 @@ def __init__( is_transparent: bool = True, default_color: AColor = TRANSPARENT, alpha: float = 1.0, - interpolation: Interpolation = Interpolation.LINEAR, + interpolation: Interpolation = "linear", **kwargs, ): super().__init__(is_transparent=is_transparent, **kwargs) @@ -259,7 +259,9 @@ def interpolation(self) -> Interpolation: @interpolation.setter def interpolation(self, interpolation: Interpolation): - self._interpolation = Interpolation(interpolation) + if interpolation not in {"nearest", "linear", "cubic", "area", "lanczos"}: + raise ValueError(f"{interpolation} is not a valid interpolation type.") + self._interpolation = interpolation def render(self, canvas_view, colors_view, source: tuple[slice, slice]): """ diff --git a/nurses_2/widgets/graphic_widget_data_structures.py b/nurses_2/widgets/graphic_widget_data_structures.py index b28b020d..6d3a1d67 100644 --- a/nurses_2/widgets/graphic_widget_data_structures.py +++ b/nurses_2/widgets/graphic_widget_data_structures.py @@ -1,45 +1,33 @@ """ Graphic widget data structures. """ -from enum import Enum from pathlib import Path +from typing import Literal import cv2 import numpy as np +from numpy.typing import NDArray from ..data_structures import Point, Size from .widget import Rect, intersection __all__ = "Interpolation", "read_texture", "resize_texture", "composite" - -class Interpolation(str, Enum): - """ - Interpolation methods for resizing graphic widgets. - - :class:`Interpolation` is one of `NEAREST`, `LINEAR`, `CUBIC`, `AREA`, - `LANCZOS`. - """ - - NEAREST = "nearest" - LINEAR = "linear" - CUBIC = "cubic" - AREA = "area" - LANCZOS = "lanczos" - +Interpolation = Literal["nearest", "linear", "cubic", "area", "lanczos"] +"""Interpolation methods for resizing graphic widgets.""" Interpolation._to_cv_enum = { - Interpolation.LINEAR: cv2.INTER_LINEAR, - Interpolation.CUBIC: cv2.INTER_CUBIC, - Interpolation.AREA: cv2.INTER_AREA, - Interpolation.LANCZOS: cv2.INTER_LANCZOS4, - Interpolation.NEAREST: cv2.INTER_NEAREST, + "linear": cv2.INTER_LINEAR, + "cubic": cv2.INTER_CUBIC, + "area": cv2.INTER_AREA, + "lanczos": cv2.INTER_LANCZOS4, + "nearest": cv2.INTER_NEAREST, } -def read_texture(path: Path) -> np.ndarray: +def read_texture(path: Path) -> NDArray[np.uint8]: """ - Return a uint8 RBGA np.ndarray from a path to an image. + Return a uint8 RGBA numpy array from a path to an image. """ image = cv2.imread(str(path.absolute()), cv2.IMREAD_UNCHANGED) @@ -58,8 +46,8 @@ def read_texture(path: Path) -> np.ndarray: def resize_texture( - texture: np.ndarray, size: Size, interpolation: Interpolation = Interpolation.LINEAR -) -> np.ndarray: + texture: NDArray[np.uint8], size: Size, interpolation: Interpolation = "linear" +) -> NDArray[np.uint8]: """ Resize texture. """ @@ -72,8 +60,8 @@ def resize_texture( def composite( - source: np.ndarray, - dest: np.ndarray, + source: NDArray[np.uint8], + dest: NDArray[np.uint8], pos: Point = Point(0, 0), mask_mode: bool = False, ): diff --git a/nurses_2/widgets/grid_layout.py b/nurses_2/widgets/grid_layout.py index df93a60a..0ba4f537 100644 --- a/nurses_2/widgets/grid_layout.py +++ b/nurses_2/widgets/grid_layout.py @@ -1,31 +1,29 @@ """ A grid layout widget. """ -from enum import Enum from itertools import accumulate, product +from typing import Literal from .widget import Size, Widget __all__ = "GridLayout", "Orientation" +Orientation = Literal[ + "lr-tb", + "lr-bt", + "rl-tb", + "rl-bt", + "tb-lr", + "tb-rl", + "bt-lr", + "bt-rl", +] +""" +Orientation of the grid. -class Orientation(str, Enum): - """ - Orientation of the grid. - - As an example, the orientation `LR_TB` means left-to-right, then top-to-bottom. - :class:`Orientation` is one of "lr-tb", "lr-bt", "rl-tb", "rl-bt", "tb-lr", "tb-rl", - "bt-lr", "bt-rl". - """ - - LR_TB = "lr-tb" - LR_BT = "lr-bt" - RL_TB = "rl-tb" - RL_BT = "rl-bt" - TB_LR = "tb-lr" - TB_RL = "tb-rl" - BT_LR = "bt-lr" - BT_RL = "bt-rl" +Describes how the grid fills as children are added. As an example, the orientation +"lr-tb" means left-to-right, then top-to-bottom. +""" class _RepositionProperty: @@ -39,9 +37,6 @@ def __get__(self, instance, owner): return getattr(instance, self.name) def __set__(self, instance, value): - if self.name == "_orientation": - value = Orientation(value) - setattr(instance, self.name, value) instance._reposition_children() @@ -65,7 +60,7 @@ class GridLayout(Widget): Number of rows. grid_columns : int, default: 1 Number of columns. - orientation : Orientation, default: Orientation.LR_TB + orientation : Orientation, default: "lr-tb" The orientation of the grid. Describes how the grid fills as children are added. The default is left-to-right then top-to-bottom. padding_left : int, default: 0 @@ -102,7 +97,7 @@ class GridLayout(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -265,8 +260,6 @@ class GridLayout(Widget): grid_columns: int = _RepositionProperty() - orientation: Orientation = _RepositionProperty() - padding_left: int = _RepositionProperty() padding_right: int = _RepositionProperty() @@ -284,7 +277,7 @@ def __init__( grid_rows: int = 1, grid_columns: int = 1, *, - orientation: Orientation = Orientation.LR_TB, + orientation: Orientation = "lr-tb", padding_left: int = 0, padding_right: int = 0, padding_top: int = 0, @@ -306,6 +299,17 @@ def __init__( super().__init__(**kwargs) + @property + def orientation(self) -> Orientation: + return self._orientation + + @orientation.setter + def orientation(self, orientation: Orientation): + if self._orientation not in Orientation.__args__: + raise TypeError(f"{orientation} is not a valid orientation.") + self._orientation = orientation + self._reposition_children() + def on_size(self): self._reposition_children() @@ -318,21 +322,21 @@ def index_at(self, row: int, col: int) -> int: cols = self.grid_columns match self.orientation: - case Orientation.LR_TB: + case "lr-tb": return col + row * cols - case Orientation.LR_BT: + case "lr-bt": return col + (rows - row - 1) * cols - case Orientation.RL_TB: + case "rl-tb": return (cols - col - 1) + row * cols - case Orientation.RL_BT: + case "rl-bt": return (cols - col - 1) + (rows - row - 1) * cols - case Orientation.TB_LR: + case "tb-lr": return row + col * rows - case Orientation.TB_RL: + case "tb-rl": return row + (cols - col - 1) * rows - case Orientation.BT_LR: + case "bt-lr": return (rows - row - 1) + col * rows - case Orientation.BT_RL: + case "bt-rl": return (rows - row - 1) + (cols - col - 1) * rows def _row_height(self, i: int) -> int: diff --git a/nurses_2/widgets/image.py b/nurses_2/widgets/image.py index 25038dbf..e52d9204 100644 --- a/nurses_2/widgets/image.py +++ b/nurses_2/widgets/image.py @@ -5,11 +5,12 @@ import cv2 import numpy as np +from numpy.typing import NDArray from .graphic_widget import GraphicWidget, Interpolation from .graphic_widget_data_structures import read_texture -__all__ = "GraphicWidget", "Interpolation" +__all__ = ("GraphicWidget",) class Image(GraphicWidget): @@ -25,7 +26,7 @@ class Image(GraphicWidget): alpha : float, default: 1.0 If widget is transparent, the alpha channel of the underlying texture will be multiplied by this value. (0 <= alpha <= 1.0) - interpolation : Interpolation, default: Interpolation.LINEAR + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -49,7 +50,7 @@ class Image(GraphicWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If false, :attr:`alpha` and alpha channels are ignored. @@ -69,7 +70,7 @@ class Image(GraphicWidget): path : pathlib.Path | None Path to image if image was loaded from a path. Setting the path immediately reloads the image. - texture : numpy.ndarray + texture : NDArray[np.uint8] uint8 RGBA color array. default_color : AColor Default texture color. @@ -149,7 +150,7 @@ class Image(GraphicWidget): Methods ------- from_texture: - Create an :class:`Image` from a uint8 rgba ndarray. + Create an :class:`Image` from a uint8 RGBA numpy array. to_png: Write :attr:`texture` to provided path as a `png` image. on_size: @@ -224,9 +225,9 @@ def on_size(self): ) @classmethod - def from_texture(cls, texture: np.ndarray, **kwargs) -> "Image": + def from_texture(cls, texture: NDArray[np.uint8], **kwargs) -> "Image": """ - Create an :class:`Image` from a uint8 rgba ndarray. + Create an :class:`Image` from a uint8 RGBA numpy array. """ kls = cls(**kwargs) kls._otexture = texture diff --git a/nurses_2/widgets/line_plot.py b/nurses_2/widgets/line_plot.py index 07ef7cce..82c9effd 100644 --- a/nurses_2/widgets/line_plot.py +++ b/nurses_2/widgets/line_plot.py @@ -125,7 +125,7 @@ class LinePlot(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/menu.py b/nurses_2/widgets/menu.py index 2c79cda5..21c7b2a4 100644 --- a/nurses_2/widgets/menu.py +++ b/nurses_2/widgets/menu.py @@ -14,9 +14,9 @@ ToggleButtonBehavior, ToggleState, ) -from .grid_layout import GridLayout, Orientation +from .grid_layout import GridLayout from .text_widget import TextWidget -from .widget import Anchor, Point, Widget +from .widget import Point, Widget __all__ = ( "Menu", @@ -89,7 +89,7 @@ class MenuItem(Themable, ToggleButtonBehavior, Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -281,7 +281,7 @@ def __init__( self.right_label = TextWidget( size=(1, wcswidth(right_label)), pos_hint=(None, 1.0), - anchor=Anchor.RIGHT_CENTER, + anchor="right", ) self.right_label.add_str(right_label) @@ -394,7 +394,7 @@ class Menu(GridLayout): Number of rows. grid_columns : int, default: 1 Number of columns. - orientation : Orientation, default: Orientation.LR_BT + orientation : Orientation, default: "tb-lr" The orientation of the grid. Describes how the grid fills as children are added. The default is left-to-right then top-to-bottom. padding_left : int, default: 0 @@ -431,7 +431,7 @@ class Menu(GridLayout): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -598,7 +598,7 @@ def __init__( self, close_on_release: bool = True, close_on_click: bool = True, - orientation=Orientation.TB_LR, + orientation="tb-lr", **kwargs, ): super().__init__(orientation=orientation, **kwargs) @@ -885,7 +885,7 @@ class MenuBar(GridLayout): Number of rows. grid_columns : int, default: 1 Number of columns. - orientation : Orientation, default: Orientation.LR_BT + orientation : Orientation, default: "lr-bt" The orientation of the grid. Describes how the grid fills as children are added. The default is left-to-right then top-to-bottom. left_padding : int, default: 0 @@ -922,7 +922,7 @@ class MenuBar(GridLayout): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/parallax.py b/nurses_2/widgets/parallax.py index b1a0adb2..4185fcff 100644 --- a/nurses_2/widgets/parallax.py +++ b/nurses_2/widgets/parallax.py @@ -5,13 +5,14 @@ from pathlib import Path import numpy as np +from numpy.typing import NDArray from ..clamp import clamp from .graphic_widget_data_structures import Interpolation from .image import Image from .widget import Widget, subscribable -__all__ = "Interpolation", "Parallax" +__all__ = ("Parallax",) def _check_layer_speeds(layers, speeds): @@ -43,7 +44,7 @@ class Parallax(Widget): where `N` is the number of layers and `i` is the index of a layer. alpha : float, default: 1.0 Transparency of the parallax. - interpolation : Interpolation, default: Interpolation.LINEAR + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -67,7 +68,7 @@ class Parallax(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: True If false, :attr:`alpha` and alpha channels are ignored. @@ -167,7 +168,7 @@ class Parallax(Widget): Methods ------- from_textures: - Create a :class:`Parallax` from an iterable of uint8 rgba ndarray. + Create a :class:`Parallax` from an iterable of uint8 RGBA numpy array. from_images: Create a :class:`Parallax` from an iterable of :class:`Image`. on_size: @@ -220,7 +221,7 @@ def __init__( path: Path | None = None, speeds: Sequence[float] | None = None, alpha: float = 1.0, - interpolation: Interpolation = Interpolation.LINEAR, + interpolation: Interpolation = "linear", is_transparent: bool = True, **kwargs, ): @@ -230,7 +231,7 @@ def __init__( paths = sorted(path.iterdir(), key=lambda file: file.name) self.layers = [Image(path=path) for path in paths] - super().__init__(is_transparent=True, **kwargs) + super().__init__(is_transparent=is_transparent, **kwargs) self.speeds = _check_layer_speeds(self.layers, speeds) self.alpha = alpha @@ -279,7 +280,8 @@ def interpolation(self) -> Interpolation: @interpolation.setter def interpolation(self, interpolation: Interpolation): - self._interpolation = Interpolation(interpolation) + if interpolation not in {"nearest", "linear", "cubic", "area", "lanczos"}: + raise ValueError(f"{interpolation} is not a valid interpolation type.") for layer in self.layers: layer.interpolation = interpolation @@ -330,13 +332,13 @@ def render(self, canvas_view, colors_view, source: tuple[slice, slice]): @classmethod def from_textures( cls, - textures: Iterable[np.ndarray], + textures: Iterable[NDArray[np.uint8]], *, speeds: Sequence[float] | None = None, **kwargs, ) -> "Parallax": """ - Create an :class:`Parallax` from an iterable of uint8 rgba ndarray. + Create an :class:`Parallax` from an iterable of uint8 RGBA numpy array. """ parallax = cls(**kwargs) parallax.layers = [ diff --git a/nurses_2/widgets/progress_bar.py b/nurses_2/widgets/progress_bar.py index a530f64f..facaa11c 100644 --- a/nurses_2/widgets/progress_bar.py +++ b/nurses_2/widgets/progress_bar.py @@ -56,7 +56,7 @@ class ProgressBar(Themable, TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. diff --git a/nurses_2/widgets/raycaster/raycaster.py b/nurses_2/widgets/raycaster/raycaster.py index d65a4f8a..c1d4b39f 100644 --- a/nurses_2/widgets/raycaster/raycaster.py +++ b/nurses_2/widgets/raycaster/raycaster.py @@ -43,7 +43,7 @@ class Raycaster(GraphicWidget): alpha : float, default: 1.0 If widget is transparent, the alpha channel of the underlying texture will be multiplied by this value. (0 <= alpha <= 1.0) - interpolation : Interpolation, default: Interpolation.LINEAR + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -67,7 +67,7 @@ class Raycaster(GraphicWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If false, :attr:`alpha` and alpha channels are ignored. @@ -105,7 +105,7 @@ class Raycaster(GraphicWidget): Floor texture. floor_color : Color Color of floor if no floor texture. - texture : numpy.ndarray + texture : NDArray[np.uint8] uint8 RGBA color array. default_color : AColor Default texture color. diff --git a/nurses_2/widgets/scroll_view/scroll_view.py b/nurses_2/widgets/scroll_view/scroll_view.py index bece04c5..cabbf482 100644 --- a/nurses_2/widgets/scroll_view/scroll_view.py +++ b/nurses_2/widgets/scroll_view/scroll_view.py @@ -61,7 +61,7 @@ class ScrollView(Grabbable, Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/shadow_caster.py b/nurses_2/widgets/shadow_caster.py index 6f776cb9..695af5e3 100644 --- a/nurses_2/widgets/shadow_caster.py +++ b/nurses_2/widgets/shadow_caster.py @@ -203,7 +203,7 @@ class ShadowCaster(GraphicWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If false, :attr:`alpha` and alpha channels are ignored. diff --git a/nurses_2/widgets/slider.py b/nurses_2/widgets/slider.py index b7c9ee6f..8beba216 100644 --- a/nurses_2/widgets/slider.py +++ b/nurses_2/widgets/slider.py @@ -61,7 +61,7 @@ class Slider(Grabbable, TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. @@ -254,7 +254,7 @@ def __init__( self._min = min self._max = max - self._handle = TextWidget(size=(1, 1), pos_hint=(0.5, None), anchor="center") + self._handle = TextWidget(size=(1, 1), pos_hint=(0.5, None)) self.add_widget(self._handle) self.handle_color_pair = handle_color_pair or self.default_color_pair self.handle_char = handle_char diff --git a/nurses_2/widgets/sparkline.py b/nurses_2/widgets/sparkline.py index d21959f6..d18fea9e 100644 --- a/nurses_2/widgets/sparkline.py +++ b/nurses_2/widgets/sparkline.py @@ -80,7 +80,7 @@ class Sparkline(TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. diff --git a/nurses_2/widgets/split_layout.py b/nurses_2/widgets/split_layout.py index 1c588014..c044e6d4 100644 --- a/nurses_2/widgets/split_layout.py +++ b/nurses_2/widgets/split_layout.py @@ -94,7 +94,7 @@ class HSplitLayout(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -366,7 +366,7 @@ class VSplitLayout(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/tabbed_widget.py b/nurses_2/widgets/tabbed_widget.py index 9e68773e..dcd8ffb8 100644 --- a/nurses_2/widgets/tabbed_widget.py +++ b/nurses_2/widgets/tabbed_widget.py @@ -96,7 +96,7 @@ class TabbedWidget(Themable, Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/text_animation.py b/nurses_2/widgets/text_animation.py index 9a06326a..5ff391b0 100644 --- a/nurses_2/widgets/text_animation.py +++ b/nurses_2/widgets/text_animation.py @@ -50,7 +50,7 @@ class TextAnimation(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text animation won't be painted. diff --git a/nurses_2/widgets/text_field.py b/nurses_2/widgets/text_field.py index 10a0e9e6..f38ff191 100644 --- a/nurses_2/widgets/text_field.py +++ b/nurses_2/widgets/text_field.py @@ -57,7 +57,7 @@ class TextParticleField(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char, background_color_pair, :attr:`particle_color_pairs` diff --git a/nurses_2/widgets/text_pad.py b/nurses_2/widgets/text_pad.py index 0daca444..2e154a5e 100644 --- a/nurses_2/widgets/text_pad.py +++ b/nurses_2/widgets/text_pad.py @@ -84,7 +84,7 @@ class TextPad(Themable, Focusable, ScrollView): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/text_panel.py b/nurses_2/widgets/text_panel.py index f57332e9..1268dc0b 100644 --- a/nurses_2/widgets/text_panel.py +++ b/nurses_2/widgets/text_panel.py @@ -53,7 +53,7 @@ class TextPanel(Themable, TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. diff --git a/nurses_2/widgets/text_widget.py b/nurses_2/widgets/text_widget.py index c6752007..2f64e1e5 100644 --- a/nurses_2/widgets/text_widget.py +++ b/nurses_2/widgets/text_widget.py @@ -2,17 +2,19 @@ A text widget. """ import numpy as np +from numpy.typing import NDArray from wcwidth import wcswidth from ..colors import WHITE_ON_BLACK, Color, ColorPair from ..data_structures import Point, Size from .text_widget_data_structures import Border, add_text from .widget import Widget -from .widget_data_structures import Anchor, Easing, PosHint, SizeHint, style_char +from .widget_data_structures import Anchor, Char, Easing, PosHint, SizeHint, style_char __all__ = ( "add_text", "Anchor", + "Border", "ColorPair", "Easing", "Point", @@ -56,7 +58,7 @@ class TextWidget(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. @@ -73,9 +75,9 @@ class TextWidget(Widget): Attributes ---------- - canvas : numpy.ndarray + canvas : NDArray[Char] The array of characters for the widget. - colors : numpy.ndarray + colors : NDArray[np.uint8] The array of color pairs for each character in :attr:`canvas`. default_char : str Default background character. @@ -263,7 +265,7 @@ def character_width(char): def add_border( self, - style: Border = Border.LIGHT, + style: Border = "light", bold: bool = False, color_pair: ColorPair | None = None, ): @@ -272,7 +274,7 @@ def add_border( Parameters ---------- - style : Border, default: Border.LIGHT + style : Border, default: "light" Style of border. Default style uses light box-drawing characters. bold : bool, default: False Whether the border is bold. @@ -421,7 +423,12 @@ def set_text( overline=overline, ) - def render(self, canvas_view, colors_view, source: tuple[slice, slice]): + def render( + self, + canvas_view: NDArray[Char], + colors_view: NDArray[np.uint8], + source: tuple[slice, slice], + ): """ Paint region given by source into canvas_view and colors_view. """ diff --git a/nurses_2/widgets/text_widget_data_structures.py b/nurses_2/widgets/text_widget_data_structures.py index 11ff248e..d3783cd7 100644 --- a/nurses_2/widgets/text_widget_data_structures.py +++ b/nurses_2/widgets/text_widget_data_structures.py @@ -1,16 +1,18 @@ """ Data structures for text widgets. """ -from enum import Enum +from typing import Literal -import numpy as np +from numpy.typing import NDArray from wcwidth import wcswidth, wcwidth +from .widget_data_structures import Char + __all__ = "add_text", "Border" def add_text( - canvas: np.ndarray, + canvas: NDArray[Char], text: str, *, bold: bool = False, @@ -28,7 +30,7 @@ def add_text( Parameters ---------- - canvas : numpy.ndarray + canvas : NDArray[Char] A 1- or 2-dimensional view of a `TextWidget` canvas. text : str Text to add to canvas. @@ -79,19 +81,7 @@ def add_text( i += width -class Border(str, Enum): - """ - Border styles for :meth:`nurses_2.text_widget.TextWidget.add_border`. - - :class:`Borders` is one of `"light"`, `"heavy"`, `"double"`, `"curved"`, - `"ascii"`, `"outer"`, `"inner"`, `"thick"`. - """ - - LIGHT = "light" - HEAVY = "heavy" - DOUBLE = "double" - CURVED = "curved" - ASCII = "ascii" - OUTER = "outer" - INNER = "inner" - EQUAL = "thick" +Border = Literal[ + "light", "heavy", "double", "curved", "ascii", "outer", "inner", "thick" +] +"""Border styles for :meth:`nurses_2.text_widget.TextWidget.add_border`.""" diff --git a/nurses_2/widgets/textbox.py b/nurses_2/widgets/textbox.py index cc6b68fa..e13f002f 100644 --- a/nurses_2/widgets/textbox.py +++ b/nurses_2/widgets/textbox.py @@ -62,7 +62,7 @@ class Textbox(Themable, Focusable, Grabbable, Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/tiled_image.py b/nurses_2/widgets/tiled_image.py index efe48b91..956a502a 100644 --- a/nurses_2/widgets/tiled_image.py +++ b/nurses_2/widgets/tiled_image.py @@ -7,6 +7,8 @@ from .graphic_widget import GraphicWidget +__all__ = ("TiledImage",) + class TiledImage(GraphicWidget): """ @@ -21,7 +23,7 @@ class TiledImage(GraphicWidget): alpha : float, default: 1.0 If widget is transparent, the alpha channel of the underlying texture will be multiplied by this value. (0 <= alpha <= 1.0) - interpolation : Interpolation, default: Interpolation.LINEAR + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -45,7 +47,7 @@ class TiledImage(GraphicWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If false, :attr:`alpha` and alpha channels are ignored. @@ -65,7 +67,7 @@ class TiledImage(GraphicWidget): tile : GraphicWidget The widget to tile. Setting this attribute updates the texture immediately. - texture : numpy.ndarray + texture : NDArray[np.uint8] uint8 RGBA color array. default_color : AColor Default texture color. diff --git a/nurses_2/widgets/toggle_button.py b/nurses_2/widgets/toggle_button.py index 7b0ff0a6..e2519be7 100644 --- a/nurses_2/widgets/toggle_button.py +++ b/nurses_2/widgets/toggle_button.py @@ -11,7 +11,7 @@ ToggleButtonBehavior, ToggleState, ) -from .text_widget import Anchor, TextWidget +from .text_widget import TextWidget from .widget import Widget CHECK_OFF = "□ " @@ -65,7 +65,7 @@ class ToggleButton(Themable, ToggleButtonBehavior, Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -237,7 +237,7 @@ def __init__( ): self.normal_color_pair = (0,) * 6 # Temporary assignment - self._label_widget = TextWidget(pos_hint=(0.5, 0), anchor=Anchor.LEFT_CENTER) + self._label_widget = TextWidget(pos_hint=(0.5, 0), anchor="left") self.callback = callback # This must be set before `super().__init__`. diff --git a/nurses_2/widgets/tree_view.py b/nurses_2/widgets/tree_view.py index 9224dc09..7ee67fea 100644 --- a/nurses_2/widgets/tree_view.py +++ b/nurses_2/widgets/tree_view.py @@ -42,7 +42,7 @@ class TreeViewNode(Themable, ButtonBehavior, TextWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background color and whitespace in text widget won't be painted. @@ -375,7 +375,7 @@ class TreeView(Widget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. diff --git a/nurses_2/widgets/video_player.py b/nurses_2/widgets/video_player.py index c88ecc5e..08be1d0c 100644 --- a/nurses_2/widgets/video_player.py +++ b/nurses_2/widgets/video_player.py @@ -14,7 +14,7 @@ from ..colors import ABLACK from .graphic_widget import GraphicWidget, Interpolation -__all__ = "Interpolation", "VideoPlayer" +__all__ = ("VideoPlayer",) _IS_WSL: bool = uname().system == "Linux" and uname().release.endswith("Microsoft") @@ -35,7 +35,7 @@ class VideoPlayer(GraphicWidget): alpha : float, default: 1.0 If widget is transparent, the alpha channel of the underlying texture will be multiplied by this value. (0 <= alpha <= 1.0) - interpolation : Interpolation, default: Interpolation.LINEAR + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -59,7 +59,7 @@ class VideoPlayer(GraphicWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If false, :attr:`alpha` and alpha channels are ignored. @@ -82,7 +82,7 @@ class VideoPlayer(GraphicWidget): If true, video will restart after last frame. is_device : bool If true, video is from a video capturing device. - texture : numpy.ndarray + texture : NDArray[np.uint8] uint8 RGBA color array. default_color : AColor Default texture color. diff --git a/nurses_2/widgets/widget.py b/nurses_2/widgets/widget.py index 5015a0a2..1b8f5408 100644 --- a/nurses_2/widgets/widget.py +++ b/nurses_2/widgets/widget.py @@ -136,7 +136,7 @@ class Widget: pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If true, background_char and background_color_pair won't be painted. @@ -279,7 +279,7 @@ def __init__( min_height: int | None = None, max_height: int | None = None, pos_hint: PosHint = PosHint(None, None), - anchor=Anchor.TOP_LEFT, + anchor: Anchor = "center", is_transparent: bool = False, is_visible: bool = True, is_enabled: bool = True, @@ -564,7 +564,9 @@ def anchor(self) -> Anchor: @anchor.setter @subscribable def anchor(self, anchor: Anchor): - self._anchor = Anchor(anchor) + if anchor not in Anchor.__args__: + raise TypeError(f"{anchor} is not a valid Anchor") + self._anchor = anchor self.apply_hints() @property @@ -633,24 +635,24 @@ def apply_hints(self): return match self.anchor: - case Anchor.TOP_LEFT: - offset_top, offset_left = 0, 0 - case Anchor.TOP_RIGHT: - offset_top, offset_left = 0, self.width - case Anchor.BOTTOM_LEFT: - offset_top, offset_left = self.height, 0 - case Anchor.BOTTOM_RIGHT: - offset_top, offset_left = self.height, self.width - case Anchor.CENTER: + case "center": offset_top, offset_left = self.center - case Anchor.TOP_CENTER: + case "top": offset_top, offset_left = 0, self.center.x - case Anchor.BOTTOM_CENTER: + case "bottom": offset_top, offset_left = self.height, self.center.x - case Anchor.LEFT_CENTER: + case "left": offset_top, offset_left = self.center.y, 0 - case Anchor.RIGHT_CENTER: + case "right": offset_top, offset_left = self.center.y, self.width + case "top-left": + offset_top, offset_left = 0, 0 + case "top-right": + offset_top, offset_left = 0, self.width + case "bottom-left": + offset_top, offset_left = self.height, 0 + case "bottom-right": + offset_top, offset_left = self.height, self.width if y_hint is not None: self.top = int(h * y_hint) - offset_top @@ -839,7 +841,12 @@ def on_paste(self, paste_event: PasteEvent) -> bool | None: or ``None``). """ - def render(self, canvas_view, colors_view, source: tuple[slice, slice]): + def render( + self, + canvas_view: NDArray[Char], + colors_view: NDArray[np.uint8], + source: tuple[slice, slice], + ): """ Paint region given by `source` into `canvas_view` and `colors_view`. """ @@ -876,7 +883,7 @@ async def tween( self, *, duration: float = 1.0, - easing: Easing = Easing.LINEAR, + easing: Easing = "linear", on_start: Callable | None = None, on_progress: Callable | None = None, on_complete: Callable | None = None, @@ -890,7 +897,7 @@ async def tween( ---------- duration : float, default: 1.0 The duration of the tween in seconds. - easing : Easing, default: Easing.LINEAR + easing : Easing, default: "linear" The easing used for tweening. on_start : Callable | None, default: None Called when tween starts. @@ -902,13 +909,13 @@ async def tween( Widget properties' target values. E.g., to smoothly tween a widget's position to (5, 10) over 2.5 seconds, specify the `pos` property as a keyword-argument: - ``await widget.tween(pos=(5, 10), duration=2.5, easing=Easing.OUT_BOUNCE)`` + ``await widget.tween(pos=(5, 10), duration=2.5, easing="out_bounce")`` Warnings -------- Running several tweens on the same properties concurrently will probably result - in unexpected behavior. `tween` won't work for ndarray types. If tweening size - or pos hints, make sure the relevant hints aren't `None` to start. + in unexpected behavior. `tween` won't work for numpy array types. If tweening + size or pos hints, make sure the relevant hints aren't `None` to start. """ end_time = monotonic() + duration start_values = tuple(getattr(self, attr) for attr in properties) diff --git a/nurses_2/widgets/widget_data_structures.py b/nurses_2/widgets/widget_data_structures.py index af961e06..15bbcad2 100644 --- a/nurses_2/widgets/widget_data_structures.py +++ b/nurses_2/widgets/widget_data_structures.py @@ -1,10 +1,10 @@ """ Data structures for widgets. """ -from enum import Enum -from typing import NamedTuple +from typing import Literal, NamedTuple import numpy as np +from numpy.typing import NDArray __all__ = "Char", "style_char", "SizeHint", "PosHint", "Rect", "Anchor", "Easing" @@ -28,7 +28,7 @@ def style_char( underline: bool = False, strikethrough: bool = False, overline: bool = False, -) -> np.ndarray: +) -> NDArray[Char]: """ Return a zero-dimensional `Char` array. @@ -156,59 +156,52 @@ class Rect(NamedTuple): right: int -class Anchor(str, Enum): - """ - Point of widget attached to :attr:`nurses_2.widgets.Widget.pos_hint`. - - :class:`Anchor` is one of `"center"`, `"left_center"`, `"right_center"`, - `"top_left"`, `"top_center"`, `"top_right"`, `"bottom_left"`, - `"bottom_center"`, `"bottom_right"`. - """ - - CENTER = "center" - LEFT_CENTER = "left_center" - RIGHT_CENTER = "right_center" - TOP_LEFT = "top_left" - TOP_CENTER = "top_center" - TOP_RIGHT = "top_right" - BOTTOM_LEFT = "bottom_left" - BOTTOM_CENTER = "bottom_center" - BOTTOM_RIGHT = "bottom_right" - - -class Easing(str, Enum): - """ - Easings for :meth:`nurses_2.widgets.Widget.tween`. - """ +Anchor = Literal[ + "bottom", + "bottom-left", + "bottom-right", + "center", + "left", + "right", + "top", + "top-left", + "top-right", +] +""" +Point of widget attached to :attr:`nurses_2.widgets.Widget.pos_hint`. +""" - LINEAR = "linear" - IN_QUAD = "in_quad" - OUT_QUAD = "out_quad" - IN_OUT_QUAD = "in_out_quad" - IN_CUBIC = "in_cubic" - OUT_CUBIC = "out_cubic" - IN_OUT_CUBIC = "in_out_cubic" - IN_QUART = "in_quart" - OUT_QUART = "out_quart" - IN_OUT_QUART = "in_out_quart" - IN_QUINT = "in_quint" - OUT_QUINT = "out_quint" - IN_OUT_QUINT = "in_out_quint" - IN_SINE = "in_sine" - OUT_SINE = "out_sine" - IN_OUT_SINE = "in_out_sine" - IN_EXP = "in_exp" - OUT_EXP = "out_exp" - IN_OUT_EXP = "in_out_exp" - IN_CIRC = "in_circ" - OUT_CIRC = "out_circ" - IN_OUT_CIRC = "in_out_circ" - IN_ELASTIC = "in_elastic" - OUT_ELASTIC = "out_elastic" - IN_OUT_ELASTIC = "in_out_elastic" - IN_BACK = "in_back" - OUT_BACK = "out_back" - IN_OUT_BACK = "in_out_back" - IN_BOUNCE = "in_bounce" - OUT_BOUNCE = "out_bounce" - IN_OUT_BOUNCE = "in_out_bounce" +Easing = Literal[ + "linear", + "in_quad", + "out_quad", + "in_out_quad", + "in_cubic", + "out_cubic", + "in_out_cubic", + "in_quart", + "out_quart", + "in_out_quart", + "in_quint", + "out_quint", + "in_out_quint", + "in_sine", + "out_sine", + "in_out_sine", + "in_exp", + "out_exp", + "in_out_exp", + "in_circ", + "out_circ", + "in_out_circ", + "in_elastic", + "out_elastic", + "in_out_elastic", + "in_back", + "out_back", + "in_out_back", + "in_bounce", + "out_bounce", + "in_out_bounce", +] +"""Easings for :meth:`nurses_2.widgets.Widget.tween`""" diff --git a/nurses_2/widgets/window.py b/nurses_2/widgets/window.py index d74c719c..3e4134d6 100644 --- a/nurses_2/widgets/window.py +++ b/nurses_2/widgets/window.py @@ -12,7 +12,7 @@ from .behaviors.themable import Themable from .graphic_widget import GraphicWidget from .text_widget import TextWidget -from .widget import Anchor, Widget +from .widget import Widget __all__ = ("Window",) @@ -23,7 +23,7 @@ def __init__(self): pos=(1, 2), disable_ptf=True, is_transparent=False, background_char=" " ) - self._label = TextWidget(pos_hint=(None, 0.5), anchor=Anchor.TOP_CENTER) + self._label = TextWidget(pos_hint=(None, 0.5), anchor="top") self.add_widget(self._label) def on_add(self): @@ -75,7 +75,7 @@ class Window(Themable, Focusable, Resizable, GraphicWidget): alpha : float, default: 1.0 If widget is transparent, the alpha channel of the underlying texture will be multiplied by this value. (0 <= alpha <= 1.0) - interpolation : Interpolation, default: Interpolation.LINEAR + interpolation : Interpolation, default: "linear" Interpolation used when widget is resized. size : Size, default: Size(10, 10) Size of widget. @@ -99,7 +99,7 @@ class Window(Themable, Focusable, Resizable, GraphicWidget): pos_hint : PosHint, default: PosHint(None, None) Position as a proportion of parent's height and width. Non-None values will have precedent over :attr:`pos`. - anchor : Anchor, default: Anchor.TOP_LEFT + anchor : Anchor, default: "center" The point of the widget attached to :attr:`pos_hint`. is_transparent : bool, default: False If false, :attr:`alpha` and alpha channels are ignored. @@ -136,7 +136,7 @@ class Window(Themable, Focusable, Resizable, GraphicWidget): Transparency of border. This value will be clamped between `0.0` and `1.0`. border_color : AColor Color of border. - texture : numpy.ndarray + texture : NDArray[np.uint8] uint8 RGBA color array. default_color : AColor Default texture color.