-
Notifications
You must be signed in to change notification settings - Fork 15
add basic UI Layout Algorithm #21
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
Open
leila79
wants to merge
21
commits into
rl-language:master
Choose a base branch
from
leila79:UI_Layout
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
3a56834
add basic UI Layout Algorithm
leila79 9ab3146
added log utility for UI layout
leila79 8899851
added UI tree visualizer
leila79 d2846d9
fix file structure and added tests
leila79 d7b1c41
test for serialization
leila79 b5c7be1
bug fix and refactoring
leila79 bf40939
add basic layout display for rl tic-tac-toe game
leila79 d1cf0db
add scene graph
leila79 38e0ee8
type-based renderer hierarchy
leila79 d5614a8
add update render for animation
leila79 dbfb8de
added renderer type serialization and bug fix
leila79 98bd2be
added user input support in UI
leila79 0e47b27
reworked a bit render mechanism
drblallo 535fd79
added cpp serializer
drblallo 9f71fc6
added config file to handle user interactions
leila79 7bc84d8
fix UI serilization to use yaml
leila79 1e48820
handle user interaction config at compile time
leila79 6641255
fix naming for path in saved renderer
leila79 cb867ea
add simulation to renderer mapping
leila79 1138632
add event queue for updating layout
leila79 97af892
add diff support in rlc stdlib
leila79 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ release | |
| .clangd | ||
| .cache | ||
| .idea | ||
| logs | ||
|
|
||
| # User-specific stuff | ||
| .idea/**/workspace.xml | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| from graphviz import Digraph | ||
| import json | ||
| import sys | ||
| from typing import Optional | ||
| import argparse | ||
|
|
||
| def visualize_layout_tree(data, output_path: str = "-", format: str = "png") -> Optional[str]: | ||
| if isinstance(data, str): | ||
| data = json.load(data) | ||
|
|
||
| tree = data['final_tree'] | ||
|
|
||
| dot = Digraph('LayoutTree', filename='layout_tree' if output_path == '-' else output_path, format=format) | ||
| dot.attr("node", shape="box", fontname="Arial", fontsize="10") | ||
|
|
||
| def add_node(node): | ||
| nid = str(node["node_id"]) | ||
leila79 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| label = f'{node["type"]}#{node["node_id"]}\n' | ||
| label += f'{node["sizing_policy"]["width"]}×{node["sizing_policy"]["height"]}\n' | ||
| label += f'dir={node.get("direction", "-")}\n' | ||
| label += f'size=({node["size"]["w"]}×{node["size"]["h"]})\n' | ||
| label += f'pos=({node["position"]["x"]},{node["position"]["y"]})' | ||
| color = node.get("color", "#dddddd") | ||
| if node["type"] == "Text": | ||
| txt = node.get("text_preview", "") | ||
| preview = (txt[:20] + "…") if len(txt) > 20 else txt | ||
| label += f'\n"{preview}"' | ||
| color = "#dddddd" | ||
| dot.node(nid, label, style="filled", fillcolor=color) | ||
| for c in node.get("children", []): | ||
| cid = str(c["node_id"]) | ||
| dot.edge(nid, cid) | ||
| add_node(c) | ||
| add_node(tree) | ||
| if output_path == '-': | ||
| return dot.source | ||
| else: | ||
| dot.render(cleanup=True) | ||
| return None | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| parser = argparse.ArgumentParser(description="Visualize layout tree from a layout JSON file.") | ||
| parser.add_argument("input", | ||
| help="Input JSON layout log (e.g., logs/layout-log.json)", nargs="?", default="-") | ||
| parser.add_argument("-o", "--out", default='-', | ||
| help="Output file path or '-' for stdout (default: '-')") | ||
| parser.add_argument("--format", default="png", choices=["png", "svg", "pdf"], | ||
| help="Output format (default: png)") | ||
|
|
||
| args = parser.parse_args() | ||
| if args.input != "-": | ||
| with open(args.input, 'r', encoding='utf-8') as f: | ||
| data = json.load(f) | ||
| else: | ||
| data = json.load("\n".join(sys.stdin.readlines())) | ||
|
|
||
| result = visualize_layout_tree(data, args.out, args.format) | ||
| if result: | ||
| print(result) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| import ctypes | ||
| from dataclasses import dataclass, field | ||
| from typing import Optional, Any, Callable, Dict | ||
| from enum import Enum | ||
|
|
||
|
|
||
| class SignalKind(Enum): | ||
| ACTION = "action" | ||
| RESIZE = "resize" | ||
| SCROLL = "scroll" | ||
| FOCUS = "focus" | ||
|
|
||
|
|
||
| @dataclass | ||
| class UpdateSignal: | ||
| """ | ||
| Represents a single event that may require state mutation and/or relayout. | ||
|
|
||
| The queue accumulates these during event polling, then processes them | ||
| in batch: all mutations first, then a single update+relayout pass. | ||
| """ | ||
| kind: SignalKind | ||
| handler_name: Optional[str] = None | ||
| args: Dict[str, Any] = field(default_factory=dict) | ||
| target: Any = None | ||
| width: int = 0 | ||
| height: int = 0 | ||
| dx: int = 0 | ||
| dy: int = 0 | ||
|
|
||
|
|
||
| def _copy_state(state_obj): | ||
| """Deep copy of the state object via RLC's generated clone() method. | ||
| clone() uses rl_m_assign which allocates independent heap storage for | ||
| Vector fields, preventing double-free and allowing diff to detect changes. | ||
| """ | ||
| return state_obj.clone() | ||
|
|
||
|
|
||
| def _rlc_string_to_python(rlc_str) -> str: | ||
| """Convert an RLC String object to a Python str.""" | ||
| vec = rlc_str._data # Vector<Byte> | ||
| size = vec._size # includes null terminator | ||
| data = vec._data # pointer to bytes | ||
| return bytes( | ||
| data[i] if isinstance(data[i], int) else data[i].value | ||
| for i in range(size - 1) # -1 to skip null terminator | ||
| ).decode('ascii') | ||
|
|
||
|
|
||
| class UpdateController: | ||
| """ | ||
| Guarded Update Protocol controller. | ||
|
|
||
| Decouples event collection from state mutation from UI update. | ||
| Prevents reentrancy by processing in strict phases: | ||
|
|
||
| Phase 1 (COLLECT): Accumulate UpdateSignals from input events | ||
| Phase 2 (MUTATE): Execute all action handlers | ||
| Phase 3 (UPDATE): Call RLC diff against last known state, | ||
| update only the renderers whose sim fields changed | ||
| Phase 4 (RELAYOUT): If dirty, recompute sizes and positions once | ||
| """ | ||
|
|
||
| def __init__(self, renderer, layout, relayout_fn: Callable, dispatch_fn: Callable, | ||
| mapping=None, state_obj=None, program_module=None): | ||
| """ | ||
| Args: | ||
| renderer: The root renderer (Renderable subclass) | ||
| layout: The root layout node | ||
| relayout_fn: Callable that recomputes layout sizes and positions | ||
| dispatch_fn: Callable(handler_name, args) -> bool that executes | ||
| an action handler and returns True if state changed. | ||
| mapping: Optional SimRendererMapping for targeted updates. | ||
| If None, falls back to full renderer.update(). | ||
| state_obj: The initial state object. Required if mapping is provided. | ||
| program_module: The compiled RLC module. Required for targeted updates | ||
| (provides the `diff` function from algorithms/diff.rl). | ||
| """ | ||
| self.renderer = renderer | ||
| self.layout = layout | ||
| self.relayout_fn = relayout_fn | ||
| self.dispatch_fn = dispatch_fn | ||
|
|
||
| self._queue = [] | ||
| self._processing = False | ||
| self._state_changed = False | ||
| self._needs_relayout = False | ||
|
|
||
| self.scroll = {"x": 0, "y": 0} | ||
|
|
||
| # Targeted update support | ||
| self._mapping = mapping | ||
| self._program_module = program_module | ||
| if mapping is not None and state_obj is not None: | ||
| self._last_state = _copy_state(state_obj) | ||
| else: | ||
| self._last_state = None | ||
|
|
||
| def enqueue(self, signal: UpdateSignal): | ||
| """Add a signal to the queue. Safe to call from handlers.""" | ||
| self._queue.append(signal) | ||
|
|
||
| def process(self, state_obj, elapsed: float): | ||
| """ | ||
| Process all queued signals. Called once per frame after all events collected. | ||
|
|
||
| Args: | ||
| state_obj: The simulation state object (e.g., state.state) | ||
| elapsed: Time since last frame in seconds | ||
| """ | ||
| if self._processing: | ||
| return | ||
|
|
||
| self._processing = True | ||
| self._needs_relayout = False | ||
|
|
||
| try: | ||
| # Phase 2: MUTATE - execute all queued signals | ||
| while self._queue: | ||
| signal = self._queue.pop(0) | ||
| self._handle_signal(signal) | ||
|
|
||
| # Phase 3: UPDATE | ||
| if self._state_changed: | ||
| can_target = (self._mapping is not None | ||
| and self._last_state is not None | ||
| and self._program_module is not None | ||
| and hasattr(self._program_module, 'diff') | ||
| and hasattr(self._program_module, 'VectorTStringT')) | ||
|
|
||
| if can_target: | ||
| self._targeted_update(state_obj, elapsed) | ||
| else: | ||
| self.renderer.update(self.layout, state_obj, elapsed) | ||
| self._needs_relayout = True | ||
|
|
||
| # Phase 4: RELAYOUT - recompute sizes/positions (once) | ||
| if self._needs_relayout or _any_child_dirty(self.layout): | ||
| self.relayout_fn() | ||
|
|
||
| finally: | ||
| self._processing = False | ||
| self._state_changed = False | ||
|
|
||
| def _targeted_update(self, state_obj, elapsed: float): | ||
| """ | ||
| Call RLC diff to find changed fields, update only those renderers. | ||
| Uses stdlib/algorithms/diff.rl via program_module.diff(). | ||
| """ | ||
| from rlc.sim_renderer_mapping import SimRendererMapping | ||
|
|
||
| changed = self._program_module.VectorTStringT() | ||
| self._program_module.diff(self._last_state, state_obj, changed) | ||
|
|
||
| num_changed = changed.size() if hasattr(changed, 'size') else changed._data._size | ||
| for i in range(num_changed): | ||
| path_str = _rlc_string_to_python(changed.get(i).contents) | ||
| sim_path = tuple( | ||
| int(p) if p.isdigit() else p | ||
| for p in path_str.split('.') | ||
| if p | ||
| ) | ||
| entry = self._mapping.get_entry(sim_path) | ||
| if entry: | ||
| value = SimRendererMapping.resolve_value(state_obj, sim_path) | ||
| entry.renderer.update(entry.layout_node, value, elapsed) | ||
|
|
||
| self._last_state = _copy_state(state_obj) | ||
| if num_changed > 0: | ||
| self._needs_relayout = True | ||
| else: | ||
| # Shallow copy shares heap with original — diff may miss heap-resident | ||
| # changes (e.g. Vector elements without size change). Fall back to | ||
| # full update so the renderer stays consistent. | ||
| self.renderer.update(self.layout, state_obj, elapsed) | ||
| self._needs_relayout = True | ||
|
|
||
| def notify_state_changed(self): | ||
| """Call after programmatic state mutations (e.g., auto-play).""" | ||
| self._state_changed = True | ||
|
|
||
| def _handle_signal(self, signal: UpdateSignal): | ||
| if signal.kind == SignalKind.ACTION: | ||
| changed = self.dispatch_fn(signal.handler_name, signal.args) | ||
| if changed: | ||
| self._state_changed = True | ||
|
|
||
| elif signal.kind == SignalKind.FOCUS: | ||
| self.layout.set_focus(signal.target) | ||
| self._needs_relayout = True | ||
|
|
||
| elif signal.kind == SignalKind.RESIZE: | ||
| self._needs_relayout = True | ||
|
|
||
| elif signal.kind == SignalKind.SCROLL: | ||
| self.scroll["x"] += signal.dx | ||
| self.scroll["y"] += signal.dy | ||
| self._needs_relayout = True | ||
|
|
||
|
|
||
| def _any_child_dirty(layout): | ||
| """Check and clear dirty flags recursively.""" | ||
| if getattr(layout, "is_dirty", False): | ||
| layout.is_dirty = False | ||
| return True | ||
| return any( | ||
| _any_child_dirty(c) | ||
| for c in layout.children | ||
| if hasattr(c, "children") | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.