Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement gap_children in Flex layout #30

Merged
merged 14 commits into from
Aug 2, 2023
17 changes: 16 additions & 1 deletion codegen/generate_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path
from typing import get_args, get_type_hints

from reprisal.styles.styles import BorderKind, Flex
from reprisal.styles.styles import BorderKind, Flex, Typography


def literal_vals(obj: object, field: str) -> tuple[str, ...]:
Expand Down Expand Up @@ -313,6 +313,11 @@ def literal_vals(obj: object, field: str) -> tuple[str, ...]:

generated_lines = [""]

generated_lines.append(
'text_white = Style(typography=Typography(style=CellStyle(foreground=Color.from_hex("#ffffff"))))'
)
generated_lines.append("")

for color, shades in COLORS.items():
for shade, hex in shades.items():
generated_lines.extend(
Expand Down Expand Up @@ -403,6 +408,16 @@ def literal_vals(obj: object, field: str) -> tuple[str, ...]:

generated_lines.append("")

for n in N:
generated_lines.append(f"gap_children_{n} = Style(layout=Flex(gap_children={n}))")

generated_lines.append("")

for j in literal_vals(Typography, "justify"):
generated_lines.append(f'text_justify_{j} = Style(typography=Typography(justify="{j}"))')

generated_lines.append("")

utils_path.write_text(
"\n".join(
[
Expand Down
17 changes: 8 additions & 9 deletions examples/stopwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,15 @@ def on_key(event: KeyPressed) -> None:
children=[
Div(
style=row | weight_none,
children=[
Text(
content="Stopwatch Example",
style=text_amber_600,
)
],
children=[Text(content="Stopwatch", style=text_amber_600)],
),
Div(
style=row | align_children_center,
style=row | align_self_stretch | align_children_center | justify_children_space_evenly,
children=[stopwatch(selected=selected_stopwatch == n) for n in range(num_stopwatches)],
on_key=on_key,
),
Div(
style=row | align_children_center,
style=row | weight_none | align_children_end,
children=[
Text(
content=dedent(
Expand Down Expand Up @@ -93,7 +88,11 @@ async def tick() -> None:

return Text(
content=f"{elapsed_time:.6f}",
style=(border_emerald_500 if running else border_rose_500)
style=(
(border_emerald_600 if selected else border_emerald_300)
if running
else (border_rose_500 if selected else border_rose_400)
)
| (border_heavy if selected else border_double)
| pad_x_2
| pad_y_1,
Expand Down
263 changes: 263 additions & 0 deletions examples/wordle.py

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion reprisal/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from reprisal._context_vars import current_event_queue
from reprisal._utils import drain_queue
from reprisal.components import AnyElement, Component, Div, component
from reprisal.control import Control
from reprisal.events import AnyEvent, KeyPressed, StateSet, TerminalResized
from reprisal.hooks.impls import UseEffect
from reprisal.input import read_keys, start_input_control, stop_input_control
Expand Down Expand Up @@ -101,8 +102,19 @@ def put_event(event: AnyEvent) -> None:
shadow = update_shadow(screen(), None)
active_effects: set[Task[None]] = set()

should_quit = False
should_bell = False

async with TaskGroup() as tg:
while True:
if should_quit:
break

if should_bell:
output_stream.write("\a")
output_stream.flush()
should_bell = False

if needs_render:
start_render = perf_counter_ns()
shadow = update_shadow(screen(), shadow)
Expand Down Expand Up @@ -188,7 +200,12 @@ def put_event(event: AnyEvent) -> None:
case KeyPressed():
for c in layout_tree.walk_from_bottom():
if c.on_key:
c.on_key(event)
r = c.on_key(event)
match r:
case Control.Quit:
should_quit = True
case Control.Bell:
should_bell = True
case StateSet():
needs_render = True
logger.debug(
Expand Down
11 changes: 9 additions & 2 deletions reprisal/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@

from pydantic import Field

from reprisal.control import Control
from reprisal.events import KeyPressed
from reprisal.styles import Style
from reprisal.styles.styles import Flex
from reprisal.types import FrozenForbidExtras

P = ParamSpec("P")

Key = str | int | None


def component(func: Callable[P, AnyElement]) -> Callable[P, Component]:
@wraps(func)
Expand All @@ -26,20 +29,24 @@ class Component(FrozenForbidExtras):
func: Callable[..., AnyElement]
args: tuple[object, ...]
kwargs: dict[str, object]
key: Key = None

def with_key(self, key: Key) -> Component:
return self.copy(update={"key": key})


class Div(FrozenForbidExtras):
type: Literal["div"] = "div"
style: Style = Field(default=Style())
children: Sequence[Component | AnyElement] = Field(default_factory=list)
on_key: Callable[[KeyPressed], None] | None = None
on_key: Callable[[KeyPressed], Control | None] | None = None


class Text(FrozenForbidExtras):
type: Literal["text"] = "text"
content: str
style: Style = Field(default=Style(layout=Flex(weight=None)))
on_key: Callable[[KeyPressed], None] | None = None
on_key: Callable[[KeyPressed], Control | None] | None = None

@property
def children(self) -> Sequence[Component | AnyElement]:
Expand Down
8 changes: 8 additions & 0 deletions reprisal/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from __future__ import annotations

from enum import Enum


class Control(Enum):
Quit = "quit"
Bell = "bell"
49 changes: 35 additions & 14 deletions reprisal/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ def first_pass(self) -> None:
layout = style.layout

# transfer margin/border/padding to dimensions
# TODO handle "auto" here -> 0, or maybe get rid of auto margins/padding?
self.dims.margin.top = style.margin.top
self.dims.margin.bottom = style.margin.bottom
self.dims.margin.left = style.margin.left
Expand All @@ -181,6 +180,7 @@ def first_pass(self) -> None:
self.dims.padding.bottom = style.padding.bottom
self.dims.padding.left = style.padding.left
self.dims.padding.right = style.padding.right

# # text boxes with auto width get their width from their content (no wrapping)
# # TODO: revisit this, kind of want to differentiate between "auto" and "flex" here, or maybe width=Weight(1) ?
if self.element.type == "text" and self.element.style.typography.wrap == "none":
Expand Down Expand Up @@ -210,16 +210,22 @@ def first_pass(self) -> None:
if style.span.height != "auto": # i.e., if it's a fixed height
self.dims.content.height = style.span.height

num_gaps = max(sum(1 for child in self.children if child.element.style.layout.position == "relative") - 1, 0)

# grow to fit children with fixed sizes
if style.span.width == "auto":
if layout.direction == "row":
self.dims.content.width += num_gaps * layout.gap_children

for child_box in self.children:
child_element = child_box.element
child_style = child_element.style
child_layout = child_style.layout

if child_style.span.width != "auto" or (
child_element.type == "text" and child_style.typography.wrap == "none"
):
# if child_style.span.width != "auto" or (
# child_element.type == "text" and child_style.typography.wrap == "none"
# ):
if child_box.dims.content.width != 0: # i.e., it has been set
if child_layout.position == "relative":
if layout.direction == "row":
# We are growing the box to the right
Expand All @@ -229,14 +235,18 @@ def first_pass(self) -> None:
self.dims.content.width = max(self.dims.content.width, child_box.dims.width())

if style.span.height == "auto":
if layout.direction == "column":
self.dims.content.width += num_gaps * layout.gap_children

for child_box in self.children:
child_element = child_box.element
child_style = child_element.style
child_layout = child_style.layout

if child_style.span.height != "auto" or (
child_element.type == "text" and child_style.typography.wrap == "none"
):
# if child_style.span.height != "auto" or (
# child_element.type == "text" and child_style.typography.wrap == "none"
# ):
if child_box.dims.content.height != 0: # i.e., it has been set
if child_layout.position == "relative":
if layout.direction == "column":
# We are growing the box downward
Expand Down Expand Up @@ -283,6 +293,9 @@ def second_pass(self) -> None:
child for child in relative_children if child.element.style.layout.weight is not None
]
num_relative_children = len(relative_children)
num_gaps = max(sum(1 for child in self.children if child.element.style.layout.position == "relative") - 1, 0)
total_gap = num_gaps * layout.gap_children

# subtract off fixed-width/height children from what's available to flex
for child in relative_children:
if child.element.style.layout.weight is None:
Expand All @@ -302,17 +315,25 @@ def second_pass(self) -> None:
):
weights: tuple[int] = tuple(child.element.style.layout.weight for child in relative_children_with_weights) # type: ignore[assignment]
if layout.direction == "row":
available_width -= total_gap

for child, flex_portion in zip(
relative_children_with_weights, partition_int(total=available_width, weights=weights)
):
child.dims.content.width = max(flex_portion - child.dims.horizontal_edge_width(), 0)

available_width = 0

elif layout.direction == "column":
available_height -= total_gap

for child, flex_portion in zip(
relative_children_with_weights, partition_int(total=available_height, weights=weights)
):
child.dims.content.height = max(flex_portion - child.dims.vertical_edge_width(), 0)

available_height = 0

elif layout.justify_children in ("space-between", "space-around", "space-evenly"):
for child in relative_children:
# TODO: if we don't do this, flex elements never get their width set if justify_children is space-*, but this seems wrong...
Expand Down Expand Up @@ -355,19 +376,19 @@ def second_pass(self) -> None:

# TODO: available width can be negative here, but that doesn't make sense
# seems to happen when this element isn't stretch but it has fixed-width children
logger.debug("available_width", w=available_width)

# justification (main axis placement)
# TODO: need to subtract total_gap here, even though it was handled above?
if layout.direction == "row":
if layout.justify_children == "center":
x += available_width // 2
x += (available_width - total_gap) // 2
elif layout.justify_children == "end":
x += available_width
x += available_width - total_gap
elif layout.direction == "column":
if layout.justify_children == "center":
y += available_height // 2
y += (available_height - total_gap) // 2
elif layout.justify_children == "end":
y += available_height
y += available_height - total_gap

if layout.justify_children == "space-between":
if layout.direction == "row":
Expand Down Expand Up @@ -428,9 +449,9 @@ def second_pass(self) -> None:
child.dims.content.x = x
child.dims.content.y = y
if layout.direction == "row":
x += child.dims.width()
x += child.dims.width() + layout.gap_children
elif layout.direction == "column":
y += child.dims.height()
y += child.dims.height() + layout.gap_children

# alignment (cross-axis placement)
# content width/height of self, but full width/height of children
Expand Down
2 changes: 2 additions & 0 deletions reprisal/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

CLEAR_SCREEN = "\x1b[2J"

BELL = "\x07"

logger = get_logger()


Expand Down
Loading