Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.analysis.exclude": [
"**/*.pyx",
"**/*.pxd"
],
"python.analysis.stubPath": "."
}
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,12 @@ from arepy_ui.markup import load_aui

handlers = {"greet": lambda: print("Hello!")}
result = load_aui("ui.aui", handlers=handlers)
ui_manager.set_root(result.root)

if result.success and result.root is not None:
ui_manager.set_root(result.root)
else:
for error in result.errors:
print(error)
```

## Components
Expand Down
8 changes: 8 additions & 0 deletions arepy_ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
from .core.animation import Animation, Animator, Easing
from .core.fonts import (
FontManager,
FontLoadRequest,
TextMetrics,
draw_text,
draw_text_centered,
get_font,
get_font_manager,
load_font,
load_fonts,
measure_text,
measure_text_ex,
unload_font,
unload_fonts,
)
from .core.node import Node
from .core.style import Spacing, Style
Expand Down Expand Up @@ -84,9 +88,13 @@
"TransitionState",
# Fonts
"FontManager",
"FontLoadRequest",
"TextMetrics",
"get_font_manager",
"load_font",
"load_fonts",
"unload_font",
"unload_fonts",
"get_font",
"draw_text",
"draw_text_centered",
Expand Down
26 changes: 15 additions & 11 deletions arepy_ui/components/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from arepy.math import check_collision_point_rec

from ..core.node import Node
from ..core.style import Spacing, Style
from ..core.style import Spacing, Style, merge_style_fields
from ..core.types import AlignItems, Color, CursorType, JustifyContent, Unit
from ..runtime import MOUSE_BUTTON_LEFT, get_runtime
from .text import Text


_BUTTON_MERGE_FIELDS = ("margin", "position", "top", "left")


class Button(Node):
def __init__(
self,
Expand All @@ -25,6 +28,7 @@ def __init__(
pressed_color: Optional[Color] = None,
pressed_scale: float = 0.98,
style: Optional[Style] = None,
font_name: Optional[str] = None,
**kwargs,
):
default_style = Style(
Expand All @@ -38,23 +42,23 @@ def __init__(
cursor=CursorType.POINTING_HAND,
)

# Merge provided style if any
if style:
# Simple merge logic (could be improved)
default_style.margin = style.margin
default_style.position = style.position
default_style.top = style.top
default_style.left = style.left
# ... copy other props
if style is not None:
merge_style_fields(default_style, style, _BUTTON_MERGE_FIELDS)

super().__init__(style=default_style, **kwargs)

self.on_click = on_click

# Add Text Child with specified font size
self.text_node = Text(text, size=font_size, color=text_color)
self.text_node = Text(
text,
size=font_size,
color=text_color,
font_name=font_name,
)
self.text_node.pickable = False # Text should not block button click
self.add_child(self.text_node)
self.text_node.parent = self
self.children.append(self.text_node)

# Button states
self.base_color = bg_color
Expand Down
17 changes: 8 additions & 9 deletions arepy_ui/components/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from arepy.engine.renderer import Rect

from ..core.node import Node
from ..core.style import Style
from ..core.style import Style, merge_style_fields
from ..core.types import Color, Unit
from ..logging import logger
from ..runtime import get_runtime
Expand Down Expand Up @@ -43,14 +43,13 @@ def __init__(
border_radius=border_radius,
)

if style:
# Merge styles
default_style.width = style.width or default_style.width
default_style.height = style.height or default_style.height
default_style.margin = style.margin
default_style.padding = style.padding
if style.border_radius > 0:
default_style.border_radius = style.border_radius
merge_style_fields(
default_style,
style,
("width", "height", "margin", "padding"),
)
if style and style.border_radius > 0:
default_style.border_radius = style.border_radius

super().__init__(style=default_style, **kwargs)

Expand Down
103 changes: 82 additions & 21 deletions arepy_ui/components/input.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from bisect import bisect_left
from typing import Callable, Optional

from arepy.engine.input import Key
Expand All @@ -6,7 +7,7 @@

from ..core.fonts import draw_text, measure_text
from ..core.node import Node
from ..core.style import Spacing, Style
from ..core.style import Spacing, Style, merge_non_default_style_fields
from ..core.types import AlignItems, Color, CursorType, Unit
from ..runtime import MouseButton, get_runtime

Expand Down Expand Up @@ -52,9 +53,36 @@ def __init__(
cursor=CursorType.IBEAM,
)

super().__init__(style=style or default_style, **kwargs)
merge_non_default_style_fields(
default_style,
style,
(
"width",
"height",
"min_width",
"max_width",
"min_height",
"max_height",
"margin",
"padding",
"align_items",
"position",
"top",
"left",
"right",
"bottom",
"visible",
"opacity",
"background_color",
"border_color",
"border_width",
"border_radius",
"cursor",
),
)

super().__init__(style=default_style, **kwargs)

self.value = value
self.placeholder = placeholder
self.on_change = on_change
self.on_submit = on_submit
Expand All @@ -66,6 +94,12 @@ def __init__(
self.focused_border_color = Color(0, 121, 241, 255)
self.default_border_color = Color(130, 130, 130, 255)

self._value = ""
self._cached_metrics_value: Optional[str] = None
self._cached_metrics_font_size: Optional[float] = None
self._prefix_widths = [0.0]
self._midpoint_widths = []

# Cursor position (0 = before first char, len(value) = after last char)
self.cursor_pos = 0

Expand All @@ -86,6 +120,45 @@ def __init__(
# Key repeat state
self._key_states = {}

self.value = value

@property
def value(self) -> str:
return self._value

@value.setter
def value(self, text: str):
self._value = text
self._invalidate_text_metrics()

def _invalidate_text_metrics(self):
self._cached_metrics_value = None
self._cached_metrics_font_size = None

def _ensure_text_metrics(self):
if (
self._cached_metrics_value == self._value
and self._cached_metrics_font_size == self.font_size
):
return

prefix_widths = [0.0]
midpoint_widths = []
for index in range(len(self._value)):
next_width = measure_text(self._value[: index + 1], self.font_size)
prefix_widths.append(next_width)
midpoint_widths.append((prefix_widths[index] + next_width) / 2)

self._prefix_widths = prefix_widths
self._midpoint_widths = midpoint_widths
self._cached_metrics_value = self._value
self._cached_metrics_font_size = self.font_size

def _get_prefix_width(self, position: int) -> float:
self._ensure_text_metrics()
clamped = max(0, min(position, len(self._value)))
return self._prefix_widths[clamped]

def _clear_selection(self):
"""Clear text selection."""
self.has_selection = False
Expand Down Expand Up @@ -310,17 +383,8 @@ def _get_char_position_at_x(self, x: float, runtime) -> int:
if relative_x <= 0:
return 0

# Find the character position
for i in range(len(self.value) + 1):
text_width = measure_text(self.value[:i], self.font_size)
if i < len(self.value):
char_width = measure_text(self.value[i], self.font_size)
if relative_x < text_width + char_width / 2:
return i
else:
if relative_x <= text_width:
return i
return len(self.value)
self._ensure_text_metrics()
return min(bisect_left(self._midpoint_widths, relative_x), len(self._value))

def handle_input(self, mouse_pos, is_click, wheel_scroll: float = 0.0) -> bool:
if not self.style.visible:
Expand Down Expand Up @@ -413,10 +477,9 @@ def _get_content_area(self):

def _ensure_cursor_visible(self):
"""Scroll to keep cursor visible."""
runtime = get_runtime()
_, _, content_w, _ = self._get_content_area()

cursor_x = measure_text(self.value[: self.cursor_pos], int(self.font_size))
cursor_x = self._get_prefix_width(self.cursor_pos)

# Scroll left if cursor is before visible area
if cursor_x < self.text_offset_x:
Expand Down Expand Up @@ -498,8 +561,8 @@ def render(self):
sel = self._get_selection_range()
if sel and self.value:
start, end = sel
sel_start_x = measure_text(self.value[:start], int(self.font_size))
sel_end_x = measure_text(self.value[:end], int(self.font_size))
sel_start_x = self._get_prefix_width(start)
sel_end_x = self._get_prefix_width(end)

runtime.renderer.draw_rectangle(
Rect(
Expand Down Expand Up @@ -529,9 +592,7 @@ def render(self):
self.show_cursor = not self.show_cursor

if self.show_cursor:
cursor_x = measure_text(
self.value[: self.cursor_pos], int(self.font_size)
)
cursor_x = self._get_prefix_width(self.cursor_pos)
runtime.renderer.draw_rectangle(
Rect(
int(content_x + cursor_x - self.text_offset_x),
Expand Down
14 changes: 4 additions & 10 deletions arepy_ui/components/listview.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,21 +409,17 @@ def render(self):
content_w = self.computed_width - padding_x * 2 - self.scrollbar_width - 4
content_h = self.computed_height - padding_y * 2

# Scissor for content
runtime.renderer.begin_scissor_mode(
int(content_x),
int(content_y),
int(content_w + self.scrollbar_width + 4),
int(content_h),
)

# Draw visible items (virtualization)
first_visible, last_visible = self._get_visible_range()
content_bottom = content_y + content_h

for i in range(first_visible, last_visible):
item = self.items[i]
item_y = content_y + i * self.item_height - self.scroll_y

if item_y + self.item_height <= content_y or item_y >= content_bottom:
continue

# Item background
is_selected = i in self._selected_indices
is_hovered = i == self._hovered_index
Expand Down Expand Up @@ -491,8 +487,6 @@ def render(self):
self.scrollbar_thumb_color,
)

runtime.renderer.end_scissor_mode()

# Render children
for child in self.children:
child.render()
Loading
Loading