Scrolling and overlays
Scroll containers, clips and layers
overflow decides what scrolls and what clips, and positioning decides what sits
over the page. The Host lays out each scroll container's full content once; the renderer
scrolls it, because a scroll position owned by Rust would always be a frame behind.
Scroll containers
A box with overflow: auto or scroll on an axis scrolls on that axis.
Its display-list entry carries a scroll container: the viewport you see through, the size of
everything inside, and which axes scroll. overflow: hidden on the other axis
clips without scrolling. Every box inside records its nearest scrolling ancestor, so the
renderer knows what moves with each scroll position.
.chat { display: flex; flex-direction: column; height: 100%; }
.messages {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
}
.composer { display: flex; gap: 8px; padding: 12px; border-top: 1px solid #e2e8f0; }
In samples/chat the message list takes the room above the composer and scrolls
vertically only. min-height: 0 matters here, as it does in a browser: without it
a flex item will not shrink below its content, and the list would push the composer off the
bottom.
samples/table scrolls the other way. Its table is at least 640px wide, so on a
480px page the 432px wrapper scrolls sideways through all 640px.
.table-wrap {
overflow-x: auto;
overflow-y: hidden;
border-radius: 8px;
}
.invoices { width: 100%; min-width: 640px; border-collapse: collapse; }
A conversation that grows
An assistant's reply arrives a piece at a time. samples/chat keeps the
conversation in signals provided as context, and Chat::receive appends each piece
to the last message. Only that message's entry changes, in its text and its size. Nothing
else in the list is sent again.
pub fn receive(mut self, chunk: &str) {
let mut messages = self.messages.write();
if let Some(last) = messages
.last_mut()
.filter(|last| last.from == Sender::Assistant)
{
last.text.push_str(chunk);
return;
}
// A piece that arrives after the user has spoken starts a new reply.
}
The test appends ", where to?" to "Sure" and checks that the layout diff names one changed entry, the reply, with no removals and no change of order.
Clipping
A box whose overflow is anything but visible clips its descendants to
its padding box, with its rounded corners. Each descendant's entry records the clip its
ancestors impose, and a click outside that clip does not reach it. In
samples/overlay a 400 by 160 banner inside a 240 by 80 preview is cut to the
preview, and a click where the banner would have been, outside the preview, misses it.
.preview {
width: 240px;
height: 80px;
overflow: hidden;
border-radius: 8px;
}
.preview-banner { width: 400px; height: 160px; background-color: #6366f1; }
Menus and dialogs
An overlay is ordinary HTML: show it with if, place it with positioning, and stack
it with z-index. Paint order and hit-testing both follow the stacking, so a click
on the backdrop reaches the backdrop and not the button under it.
if show_dialog {
div { id: "backdrop", class: "backdrop", onclick: move |_| dialog_open.set(false) }
div { id: "dialog", class: "dialog", role: "dialog",
h2 { id: "dialog-title", "Delete project?" }
p { "This cannot be undone." }
div { class: "actions",
button { id: "cancel", onclick: move |_| dialog_open.set(false), "Cancel" }
button {
id: "confirm",
class: "danger",
onclick: move |_| {
deleted.set(true);
dialog_open.set(false);
},
"Delete"
}
}
}
}
.backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
background-color: #0f172a;
opacity: 0.5;
}
A position: fixed box is placed against the viewport wherever it sits in the
document, so the backdrop covers the whole window and the dialog is centred in it.
Not yet
- Scrolling on screen. partial In the plan a scroll container becomes a Compose scroll container holding its content, and the renderer will own the scroll position. Nothing scrolls until the renderer bridge lands (#43).
- Animated overlays. planned A menu or dialog that fades or slides in with a CSS transition does not animate today: its animated properties stay at their first value (#46).