From: mar77i Date: Fri, 9 May 2025 23:44:47 +0000 (+0200) Subject: basic sketch out for bookpaint X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=8ae9622932ab4eacb85553a5872a073c48fe8fa4;p=zenbook_gui basic sketch out for bookpaint --- diff --git a/bookpaint/bookpaint.py b/bookpaint/bookpaint.py index 473f03d..e42e614 100644 --- a/bookpaint/bookpaint.py +++ b/bookpaint/bookpaint.py @@ -1,3 +1,4 @@ +from functools import partial from pathlib import Path import pygame @@ -5,15 +6,19 @@ import pygame from ui import Root from .book_manager import BookManager +from .color_menu import ColorMenu from .draw_image import DrawImage +from .line_menu import LineMenu from .menu import Menu +from .page_menu import PageMenu # - color menu; set background (default for new pages) and foreground color -# - stub with a 6 hex-digit textinputs and a little rectangle +# - stub with a 6 hex-digit textinput and a little rectangle to show the color # - page menu; browse through next and prev pages using stylus +# - line menu: choose line width and stroke method # - info layer (F3) -# - list color and page menus in the esc menu but also using keyboard shortcuts c/p +# - list color and page menus in the main menu but also using keyboard shortcuts c/p/l class BookPaint(Root): @@ -25,15 +30,18 @@ class BookPaint(Root): pygame.display.set_mode((0, 0), pygame.FULLSCREEN), pygame.font.Font(None, size=96), ) - self.menu = Menu(self) + self.color_menu = ColorMenu(self) self.draw_image = DrawImage(self, "white") - book_path = Path("/dev/shm/book") + self.line_menu = LineMenu(self) + self.page_menu = PageMenu(self) + self.menu = Menu(self) + book_path = Path("book") if not book_path.exists(): book_path.mkdir(0o755) self.book_manager = BookManager(book_path) - def key_escape(self): - self.menu.activate() + def show_menu(self, menu_attr): + getattr(self, menu_attr).activate() def save_file(self): self.book_manager.save_file(self.draw_image.surface) @@ -56,7 +64,12 @@ class BookPaint(Root): self.set_image(self.book_manager.prev_file()) KEY_METHODS = { - frozenset(): {pygame.K_ESCAPE: key_escape}, + frozenset(): { + pygame.K_ESCAPE: partial(show_menu, menu_attr="menu"), + pygame.K_c: partial(show_menu, menu_attr="color_menu"), + pygame.K_l: partial(show_menu, menu_attr="line_menu"), + pygame.K_p: partial(show_menu, menu_attr="page_menu"), + }, frozenset({pygame.KMOD_CTRL}): { pygame.K_s: save_file, pygame.K_PAGEDOWN: next_file, diff --git a/bookpaint/color_menu.py b/bookpaint/color_menu.py new file mode 100644 index 0000000..ead9b8d --- /dev/null +++ b/bookpaint/color_menu.py @@ -0,0 +1,67 @@ +import pygame + +from ui import Button, Label, Modal, TextInput + + +class ColorMenu(Modal): + def __init__(self, parent): + super().__init__(parent) + size = self.surf.get_size() + self.size_third = tuple(a // 3 for a in size) + button_height = 128 + y = self.size_third[1] + Label( + self, + pygame.Rect((self.size_third[0], y), (self.size_third[0], 128)), + "Color Menu", + Label.HAlign.CENTER, + ) + y += button_height + 16 + self.input = TextInput( + self, + pygame.Rect((self.size_third[0], y), (self.size_third[0], 128)), + self.set_color, + "", + ) + y += (button_height + 16) * 2 + Button( + self, + pygame.Rect((self.size_third[0], y), (self.size_third[0], 128)), + "Close", + self.deactivate + ) + + def activate(self): + super().activate() + self.input.value = f"{int(self.parent.draw_image.color) >> 8:06x}" + + def set_color(self, value): + color = self.parent.draw_image.color + try: + color = pygame.Color(f"0x{value}") + except ValueError: + try: + color = pygame.Color(value) + except ValueError: + pass + self.parent.draw_image.color = color + + def draw_modal(self): + super().draw_modal() + rect = pygame.Rect(self.size_third, self.size_third) + rect.inflate_ip((32, 32)) + pygame.draw.rect(self.surf, "black", rect) + pygame.draw.rect(self.surf, "gray", rect, 1) + + def draw(self): + super().draw() + pygame.draw.rect( + self.surf, + self.parent.draw_image.color, + pygame.Rect( + (self.size_third[0], self.size_third[1] + 288), + (self.size_third[0], 128), + ), + ) + + KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: Modal.deactivate}} diff --git a/bookpaint/draw_image.py b/bookpaint/draw_image.py index 46b9075..a55dc18 100644 --- a/bookpaint/draw_image.py +++ b/bookpaint/draw_image.py @@ -6,37 +6,50 @@ from ui import Child from vectors import StrokeRoundLine, StrokeSquareLine -class StrokeMethod(Enum): - ROUND = auto() - SQUARE = auto() - PYGAME = auto() - - class DrawImage(Child): - FINGER_TIMEOUT = 30 + class StrokeMethod(Enum): + ROUND = auto() + SQUARE = auto() + PYGAME = auto() + + DEFAULT_STROKE_METHOD = StrokeMethod.ROUND def __init__(self, parent, color): super().__init__(parent) + if not isinstance(color, pygame.Color): + color = pygame.Color(color) self.color = color self.line_width = 1 - - self.surface = pygame.Surface(self.surf.get_size(), 0, 24) - self.clear() + self.image = pygame.Surface(self.surf.get_size(), 0, 24) + self.stroke_method = self.DEFAULT_STROKE_METHOD self.image_dirty = False - self.stroke_method = next(iter(StrokeMethod)) self.prev_pos = None + self.clear() def clear(self): - self.surface.fill("black") + self.image.fill("black") self.image_dirty = False self.dirty = True + self.prev_pos = None - def dot(self, pos): - pygame.draw.circle(self.surf, self.color, pos, self.line_width // 2) + def dot(self, surf, pos): + if self.stroke_method == self.StrokeMethod.SQUARE: + hw = self.line_width // 2 + pygame.draw.rect( + surf, + self.color, + pygame.Rect( + (pos[0] - hw, pos[1] - hw), (self.line_width, self.line_width) + ) + ) + elif self.line_width // 2 == 0: + surf.set_at(pos, self.color) + else: + pygame.draw.circle(surf, self.color, pos, self.line_width // 2) def draw(self): - self.surf.blit(self.surface, (0, 0)) - self.dot(pygame.mouse.get_pos()) + self.surf.blit(self.image, (0, 0)) + self.dot(self.surf, pygame.mouse.get_pos()) def handle_mousebuttondown(self, ev): if ev.button == 1: @@ -44,47 +57,32 @@ class DrawImage(Child): self.dirty = True def handle_mousemotion(self, ev): - if self.prev_pos is not None: - if self.stroke_method == StrokeMethod.ROUND: - if ev.pos == self.prev_pos: - self.dot(ev.pos) - else: - StrokeRoundLine( - self.prev_pos, ev.pos, self.line_width - ).draw(self.surface, self.color) - elif self.stroke_method == StrokeMethod.SQUARE: - if ev.pos == self.prev_pos: - pygame.draw.rect( - self.surface, - self.color, - pygame.Rect( - ( - ev.pos[0] - self.line_width // 2, - ev.pos[1] - self.line_width // 2, - ), - (self.line_width, self.line_width), - ) - ) - else: - StrokeSquareLine( - self.prev_pos, ev.pos, self.line_width - ).draw(self.surface, self.color) - elif self.stroke_method == StrokeMethod.PYGAME: - if ev.pos == self.prev_pos: - self.dot(ev.pos) - else: - pygame.draw.line( - self.surface, self.color, self.prev_pos, ev.pos, self.line_width - ) - self.prev_pos = ev.pos - self.image_dirty = True self.dirty = True + if self.prev_pos is None: + return + self.image_dirty = True + if ev.pos == self.prev_pos: + self.dot(self.image, ev.pos) + return + if self.stroke_method == self.StrokeMethod.ROUND: + StrokeRoundLine( + self.prev_pos, ev.pos, self.line_width + ).draw(self.image, self.color) + elif self.stroke_method == self.StrokeMethod.SQUARE: + StrokeSquareLine( + self.prev_pos, ev.pos, self.line_width + ).draw(self.image, self.color) + elif self.stroke_method == self.StrokeMethod.PYGAME: + pygame.draw.line( + self.image, self.color, self.prev_pos, ev.pos, self.line_width + ) + self.prev_pos = ev.pos def handle_mousebuttonup(self, ev): - if ev.button == 1 and self.prev_pos is not None: + if ev.button == 1: self.handle_mousemotion(ev) self.prev_pos = None def set_image(self, surf): - self.surface.blit(surf, (0, 0)) + self.image.blit(surf, (0, 0)) self.dirty = True diff --git a/bookpaint/line_menu.py b/bookpaint/line_menu.py new file mode 100644 index 0000000..a3468d5 --- /dev/null +++ b/bookpaint/line_menu.py @@ -0,0 +1,78 @@ +import pygame + +from ui import DropDown, Label, Modal, Slider, TextInput + +from .draw_image import DrawImage + + +class LineMenu(Modal): + def __init__(self, parent): + super().__init__(parent) + size = self.surf.get_size() + self.size_third = tuple(a // 3 for a in size) + button_height = 128 + x = self.size_third[0] + y = self.size_third[1] + Label( + self, + pygame.Rect((x, y), (x, button_height)), + "Line Menu", + Label.HAlign.CENTER, + ) + y += button_height + 16 + self.slider = Slider( + self, + pygame.Rect((x, y), (x, button_height)), + Slider.Direction.HORIZONTAL, + 0, + button_height, + self.set_line_width, + ) + y += button_height + 16 + self.input = TextInput( + self, + pygame.Rect((x, y), (x, button_height)), + self.set_line_width, + "", + ) + y += button_height + 16 + self.stroke_methods = list(DrawImage.StrokeMethod) + self.stroke_method = DropDown( + self, + pygame.Rect((self.size_third[0], y), (x, 128)), + self.parent.draw_image.stroke_method.name.title(), + [m.name.title() for m in self.stroke_methods], + self.set_stroke_method, + ) + + def activate(self): + super().activate() + self.update_value(self.parent.draw_image.line_width) + + def set_line_width(self, value): + if isinstance(value, str): + value = int(value) + if value <= 0: + value = 1 + self.parent.draw_image.line_width = value + self.update_value(value) + + def set_stroke_method(self, value): + value = self.stroke_methods[value] + self.parent.draw_image.stroke_method = value + self.stroke_method.value = value.name.title() + + def update_value(self, value): + self.slider.value = value + self.input.value = str(value) + self.dirty = True + + def draw_modal(self): + super().draw_modal() + rect = pygame.Rect(self.size_third, self.size_third) + rect.inflate_ip((32, 32)) + pygame.draw.rect(self.surf, "black", rect) + pygame.draw.rect(self.surf, "gray", rect, 1) + + + KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: Modal.deactivate}} diff --git a/bookpaint/menu.py b/bookpaint/menu.py index 3b836b6..cbfe73b 100644 --- a/bookpaint/menu.py +++ b/bookpaint/menu.py @@ -1,21 +1,71 @@ +from functools import partial + import pygame -from ui import ( - Button, - Modal, -) +from ui import Button, Modal class Menu(Modal): def __init__(self, parent): + from .bookpaint import BookPaint + + self.parent: BookPaint super().__init__(parent) - size = self.root.surf.get_size() - width_third = size[0] // 3 + self.suspended = False + size = self.surf.get_size() + self.size_third = tuple(a // 3 for a in size) + height_two_thirds = size[1] * 2 // 3 + self.button_height = 128 Button( self, - pygame.Rect((width_third, size[1] * 2 // 3), (width_third, 128)), + pygame.Rect( + (self.size_third[0], height_two_thirds), + (self.size_third[0], self.button_height), + ), "Quit", self.root.handle_quit, ) + y = size[1] // 3 + Button( + self, + pygame.Rect( + (self.size_third[0], y), (self.size_third[0], self.button_height) + ), + "Color", + partial(self.parent.show_menu, menu_attr="color_menu"), + ) + y += self.button_height + 16 + Button( + self, + pygame.Rect( + (self.size_third[0], y), (self.size_third[0], self.button_height) + ), + "Line", + partial(self.parent.show_menu, menu_attr="line_menu"), + ) + y += self.button_height + 16 + Button( + self, + pygame.Rect( + (self.size_third[0], y), (self.size_third[0], self.button_height) + ), + "Page", + partial(self.parent.show_menu, menu_attr="page_menu"), + ) + assert y + self.button_height < height_two_thirds + + def draw(self): + if self.root.focus_stack[-1] is self: + super().draw() + + def draw_modal(self): + super().draw_modal() + rect = pygame.Rect( + self.size_third, + (self.size_third[0], self.size_third[1] + self.button_height), + ) + rect.inflate_ip((32, 32)) + pygame.draw.rect(self.surf, "black", rect) + pygame.draw.rect(self.surf, "gray", rect, 1) KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: Modal.deactivate}} diff --git a/bookpaint/page_menu.py b/bookpaint/page_menu.py new file mode 100644 index 0000000..2f19c8f --- /dev/null +++ b/bookpaint/page_menu.py @@ -0,0 +1,14 @@ +import pygame + +from ui import Label, Modal + + +class PageMenu(Modal): + def __init__(self, parent): + super().__init__(parent) + Label(self, pygame.Rect((100, 100), (200, 128)), "Page Menu") + + def draw_modal(self): + pass + + KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: Modal.deactivate}} diff --git a/layout/__init__.py b/layout/__init__.py deleted file mode 100644 index ebec713..0000000 --- a/layout/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .bar_layout import BarLayout -from .grid_layout import GridLayout - -__all__ = ["BarLayout", "GridLayout"] diff --git a/layout/bar_layout.py b/layout/bar_layout.py deleted file mode 100755 index 932ebc4..0000000 --- a/layout/bar_layout.py +++ /dev/null @@ -1,19 +0,0 @@ -import pygame - - -class BarLayout: - def __init__(self, rect, pad): - self.rect = rect - self.pad = pad - self.rects = [] - - def get_rect(self): - rect = pygame.Rect((0, self.rect.top), (0, self.rect.height)) - self.rects.append(rect) - len_rects = len(self.rects) - left = self.rect.left + self.pad // 2 - width = (self.rect.width - len_rects * self.pad) // len_rects - for i, r in enumerate(self.rects): - r.left = left + i * (self.pad + width) - r.width = width - return rect diff --git a/layout/grid_layout.py b/layout/grid_layout.py deleted file mode 100644 index 38b144b..0000000 --- a/layout/grid_layout.py +++ /dev/null @@ -1,26 +0,0 @@ -import pygame - - -class GridLayout: - def __init__(self, rect, size): - self.rect = rect - self.size = size - - def get_rect(self, pos, span=(1, 1)): - if pos[0] < 0 or pos[0] >= self.size[0] or pos[1] < 0 or pos[1] >= self.size[1]: - raise ValueError(f"coordinate not on grid: {pos}") - if ( - span[0] < 1 - or pos[0] + span[0] > self.size[0] - or span[1] < 1 - or pos[0] + span[0] > self.size[0] - ): - raise ValueError(f"span exceeds the grid: {span}") - size = (self.rect.width / self.size[0], self.rect.height / self.size[1]) - return pygame.Rect( - ( - int(self.rect.left + pos[0] * size[0]), - int(self.rect.top + pos[1] * size[1]), - ), - (size[0] * span[0], size[1] * span[1]), - ) diff --git a/ui/slider.py b/ui/slider.py index f45145b..7ce43e1 100644 --- a/ui/slider.py +++ b/ui/slider.py @@ -1,3 +1,4 @@ +from enum import Enum, auto import pygame from .child import Child @@ -11,7 +12,7 @@ class HorizontalSlider(Child): super().__init__(parent) self.rect = rect self.extent = ( - self._get_extent_base() - 1 if handle_size is None else handle_size + self._get_extent_base() - (1 if handle_size is None else handle_size) ) self.value = value self.handle_size = handle_size @@ -178,12 +179,13 @@ class VerticalSlider(HorizontalSlider): class Slider: - HORIZONTAL = 0 - VERTICAL = 1 + class Direction(Enum): + HORIZONTAL = auto() + VERTICAL = auto() def __new__(cls, parent, rect, direction, value=0, handle_size=None, callback=None): - if direction == cls.HORIZONTAL: + if direction == cls.Direction.HORIZONTAL: klass = HorizontalSlider - else: # direction == cls.VERTICAL + else: # direction == cls.Direction.VERTICAL klass = VerticalSlider return klass(parent, rect, value, handle_size, callback)