Skip to content

Commit

Permalink
Implement gap_children in Flex layout (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshKarpel authored Aug 2, 2023
1 parent 0dfecf1 commit 3bea194
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 77 deletions.
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

0 comments on commit 3bea194

Please sign in to comment.