Skip to content

layout: edge-inset, overlay, and view-that-fits primitives - #70

Merged
RCmerci merged 4 commits into
mainfrom
devin/1790578572-edge-chrome
Sep 29, 2026
Merged

RCmerci merged 4 commits into
mainfrom
devin/1790578572-edge-chrome

Conversation

@RCmerci

@RCmerci RCmerci commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds three general-purpose layout containers so apps can compose pinned chrome, floating controls, and fit-or-fallback content declaratively (replaces host-side journal-chrome-style extensions; apps keep supplying their own titles/buttons/payloads):

  • edge-inset — pins children after the first to a screen edge via .safeAreaInset(edge:) while children[0] scrolls beneath. New edge prop (required, top|bottom|leading|trailing) picks the edge; new visible bool toggles the pinned region in place so the safe-area insertion animates rather than snapping; existing gap maps to the inset spacing. Surface props style the pinned region — LUIUnmodifiedNodePolicy.bypassesSurface now covers edgeInset, and the pinned stack applies LUISurfaceModifier itself. background gains the bar material name (Material.bar) alongside glass/glass-container.
  • overlay — children[0] is the base and sizes the view; children[1:] float over it via .overlay(alignment:). New alignment prop (9-point grid top-leading…bottom-trailing) on each overlay child picks its anchor, falling back to the overlay's own alignment (default center). This is distinct from stack's popover-side anchor props (above|below|left|right), which can't express inside-corner placement — so a new kind rather than extending stack.
  • view-that-fits — renders ViewThatFits(in:), picking the first child that fits; reuses orientation (horizontal|vertical, default horizontal) as the axis.

Validation

edge is required on edge-inset (node_properties_supported / validateNodeProperties on all backends); edge, visible, and alignment vocabularies are closed. alignment is allowed on any non-root kind — it's a position hint honored on overlay children (and the overlay itself as the default), inert elsewhere, same pattern as container-relative-frame.

Other backends

Flutter: Stack + Positioned.fill(Align(...)) approximations. Qt: LuiEdgeInset/LuiOverlay/LuiViewThatFits QML (anchors-based). WinUI: LUIGrid stack approximation. Web: grid-overlay CSS classes. No new events; no journal-specific naming or payload fields.

Verified

opam exec -- dune build, dune runtest -j 4 (30 tests incl. new edge/overlay/fit rules case), make test-apple (136 tests incl. 3 new parity tests) all pass.

Link to Devin session: https://app.devin.ai/sessions/052ab2a0c5a74f2ebb311241b9bf344f
Open in Devin Desktop: https://app.devin.ai/desktop/session/052ab2a0c5a74f2ebb311241b9bf344f?variant=devin
Requested by: @RCmerci

Adds three general-purpose container kinds so apps can compose pinned
chrome, floating controls, and fit-or-fallback content without host-side
hacks:

- edge-inset (EdgeInset): pins children after the first to a screen edge
  via .safeAreaInset(edge:) while the first child scrolls beneath. New
  'edge' prop (required; top|bottom|leading|trailing) picks the edge,
  new 'visible' bool animates the pinned region in/out, 'gap' maps to
  safeAreaInset spacing. Surface props style the pinned region (the node
  bypasses the container surface modifier); 'background' gains the 'bar'
  material name.
- overlay (Overlay): children[0] is the base and sizes the view; later
  children float over it via .overlay(alignment:). New 'alignment' prop
  (9-point grid: top-leading..bottom-trailing) on each child picks its
  anchor, falling back to the overlay's own alignment (default center).
  Distinct from stack's popover anchors (above|below|left|right), which
  can't express inside-corner placement.
- view-that-fits (ViewThatFits): renders ViewThatFits(in:), picking the
  first child that fits; reuses 'orientation' (horizontal|vertical,
  default horizontal) as the axis.

Validation parity across OCaml/Swift/Dart/C++/C#: edge-inset requires
'edge', vocabularies are closed, 'alignment' is allowed on any non-root
kind (position hint on overlay children; inert elsewhere).

Flutter/Qt/WinUI approximate (Stack+Positioned, anchors, LUIGrid); web
maps to grid-overlay CSS. OCaml constructors: Lui_elements.edge_inset,
overlay, view_that_fits, plus align combinator.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T07:06:25.578691Z e6cbe8c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6cbe8c12c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +20 to +25
case LUINodeKind.EdgeInset:
case LUINodeKind.Overlay:
case LUINodeKind.ViewThatFits:
// No WinUI pinned/overlay/fit primitive; children stack
// in a grid (overlay-like approximation).
return new LUIGrid();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route the new WinUI containers through child synchronization

Although these cases create an LUIGrid, the SyncKind switch in LUIElement.Sync.cs has no EdgeInset, Overlay, or ViewThatFits case, so neither SyncPlainContainer nor SyncStackLike ever inserts their children. On WinUI, all three newly exposed containers therefore render empty rather than providing the stated grid approximation.

Useful? React with 👍 / 👎.

Comment on lines +19 to +21
| EdgeInset -> "lui-edge-inset"
| Overlay -> "lui-overlay"
| ViewThatFits -> "lui-view-that-fits"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Implement DOM handling for the new layout properties

Registering these web node kinds is insufficient because Lui_web_props.apply_secondary_property has no cases for EdgeValue, Visible, or AlignmentValue and falls through to invalid_arg. Every edge_inset constructed through the public API necessarily emits its required edge property, so applying its patch aborts immediately; overlays using ~alignment or align fail the same way.

Useful? React with 👍 / 👎.

Comment thread src/lui_protocol.ml Outdated
Comment on lines +805 to +807
(* Honored on [overlay] children (position hint) and on [overlay] itself
as the default; inert elsewhere, like [container-relative-frame]. *)
| AlignmentValue -> kind <> Root

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Admit alignment through restrictive property matrices

The AlignmentValue arm does not actually make alignment valid on every non-root kind: property_supported handles restrictive kinds such as TimelineItem, Toolbar, BottomTab, and Accordion before calling common_property_supported. Consequently, valid-looking uses such as overlay [base; align top_trailing (timeline_item ...)]raise fromLui_runtime.set_prop` during emission, and the platform validators mirror the same restrictive matrices; alignment must be special-cased before that dispatch or included in each matrix.

Useful? React with 👍 / 👎.

Comment on lines +1977 to +1981
if (visible)
for (final pinned in children.skip(1))
Positioned.fill(
child: Align(alignment: alignment, child: pinned),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stack Flutter edge-inset children instead of overlapping them

When an edge inset has two or more pinned children, each child is independently placed in a full-size Positioned.fill with the same alignment, so they occupy the same coordinates and obscure one another; the advertised gap is also unused. The pinned children need to be grouped in an edge-appropriate Row or Column so every child after the base remains visible.

Useful? React with 👍 / 👎.

Comment on lines +1953 to +1957
'top-leading' => Alignment.topLeft,
'top' => Alignment.topCenter,
'top-trailing' => Alignment.topRight,
'leading' => Alignment.centerLeft,
'trailing' => Alignment.centerRight,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use directional Flutter alignments for leading and trailing

In an RTL Flutter app, mapping the protocol's leading and trailing values to physical Alignment.*Left and Alignment.*Right places overlay content on the wrong side; the edge-inset mapping below makes the same physical-side assumption. Use AlignmentDirectional/start-end alignments so these values follow Directionality, as the Swift implementation's leading/trailing edges do.

Useful? React with 👍 / 👎.

Comment on lines +5900 to +5903
if name == "bar" {
// System chrome material (`.background(.bar)`), for edge-pinned
// bars that should pick up the platform bar treatment.
content.background(.bar, in: shape)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Define the new bar background on every backend

The public edge_inset API advertises ~background:"bar", but only this Swift branch recognizes that new name. The Flutter color resolver falls back to transparent, Qt's palette falls back to transparent, WinUI's theme resolver returns no brush, and the web emits the invalid CSS color bar, so the advertised pinned chrome has no background on every non-Apple backend; each backend needs a suitable bar-surface fallback.

Useful? React with 👍 / 👎.

Comment thread src/lui_elements.ml
mount_children context node children;
node

let edge_inset ?key ?gap ?main ?cross ?grow ?columns ?padding ?padding_horizontal ?padding_vertical ?background ?foreground ?border_color ?border_width ?corner_radius ?width ?height ?min_width ?max_width ?min_height ?max_height ?container_relative_frame ?container_relative_frame_inset ?accessibility_identifier ?accessibility_identifier_signal ?foreground_signal ?background_signal ?style_class ?on_appear ~edge ?visible ?visible_signal (children : t list) : t =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Permit foreground styling on the new containers

All three new constructors expose ~foreground and ~foreground_signal, and edge-inset explicitly applies its surface modifier to the pinned region, but common_property_supported omits EdgeInset, Overlay, and ViewThatFits from the ForegroundValue whitelist. Passing either foreground option therefore raises from Lui_runtime.set_prop before a patch is emitted; the new kinds must be added consistently to the OCaml and host property validators.

Useful? React with 👍 / 👎.

Comment thread platform/web/src/lui.css Outdated
Comment on lines +409 to +412
.lui-edge-inset > *,
.lui-overlay > *,
.lui-view-that-fits > * {
grid-area: 1 / 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude web overlay children from intrinsic sizing

CSS Grid computes the shared track's intrinsic size from every item assigned to it, so a large second or later overlay child enlarges .lui-overlay even though the API promises that only the first child sizes the view. Overlay children need positioning or another mechanism that removes their intrinsic contribution while retaining the base child as the sizing element.

Useful? React with 👍 / 👎.

Comment thread platform/qt/qml/LuiOverlay.qml Outdated
Comment on lines +67 to +71
onItemAdded: function(index, item) {
if (index === 0) {
item.anchors.fill = overlay
} else {
overlay.anchorOverlay(item)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-anchor Qt overlays when alignment properties change

The anchors are assigned only from Repeater.onItemAdded, so if a retained overlay or child receives a new alignment value during dynamic reconciliation, the delegate remains at its original position because no item is added and anchorOverlay is not rerun. Use bindings or property-change handlers that also clear obsolete anchors; LuiEdgeInset.qml has the same issue when its retained edge property changes.

Useful? React with 👍 / 👎.

Edge Inset page toggles the pinned region via 'visible' and shows top
and bottom bars over scrolling lists; Overlay floats aligned children
over a base card; View That Fits swaps expanded/compact variants on
window resize.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Verified all three new container kinds end-to-end in the native macOS LUIComponentsApp gallery host (built liblui_components.dylib + LUIComponentsApp, drove the real app window):

Check Result
edge-inset edge=bottom pinned bar bar pinned at bottom, list scrolls above
visible false→true pinned region animates in/out in place, scroll state preserved
edge-inset edge=top + background="bar" material bar stays fixed while rows scroll beneath
overlay per-child alignment badges float at top-trailing / bottom-leading without shifting base layout
view-that-fits on resize EXPANDED (640pt) ↔ COMPACT fallback as window narrows/widens
Screenshots

Pinned top bar with list scrolled beneath
Overlay badges at top-trailing and bottom-leading
ViewThatFits compact fallback at narrow width

Demo pages are committed in examples/gallery/view.ml for reproduction.

- WinUI: route the new kinds through SyncStackLike so children render
- Web: handle edge/visible/alignment in apply_secondary_property and
  remove_property; overlay children go position:absolute so they no
  longer contribute intrinsic size to the grid
- Protocol validators (OCaml/Swift/Qt/WinUI/Flutter): admit 'alignment'
  ahead of the restrictive kindProperties matrices, and whitelist the
  new kinds for 'foreground'
- Flutter: group pinned edge-inset children in an edge-appropriate
  Row/Column, honor 'gap', and use AlignmentDirectional so
  leading/trailing follow RTL
- background:'bar' gets a translucent surface fallback on Flutter, Qt,
  WinUI, and web
- Qt: bind anchors declaratively so retained delegates re-anchor when
  alignment/edge changes
A pinned child that expands to its proposal (e.g. overlay's ZStack) made
safeAreaInset consume the whole screen, collapsing the scrolling base
content to zero height. Hug the pinned stack along the edge axis.
@RCmerci
RCmerci merged commit 47580c4 into main Sep 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant