Primitives · a raise keeps its state under its owner's id

A raise inside another was namespaced by its place in the layer's
queue, so one that came before it moved its id path and rebuilt its
keyed state and focus: an open date popup would lose its month when a
tip appeared. raise now takes its owner's id and draws under it, and
float and float_height pass the owner's id on from every call site.
A test holds a focused box in a raise while another comes before it.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 02:23:05 -04:00
1 parent 341420d1e4
commit 51ade29032
25 files changed
+188 -40

No files matched your search

+2 -1
View File
@@ -76,12 +76,13 @@ impl RenderOnce for Backdrop {
scrim.on_mouse_down(MouseButton::Left, move |_, window, cx| dismiss(window, cx))
})
.with_animation(
self.id,
self.id.clone(),
Animation::new(fade).with_easing(motion::ease_out_cubic),
|scrim, t| scrim.opacity(t),
);
raise(
(self.id, "raised"),
anchored().position(Point::default()).child(
div()
.relative()
+16 -15
View File
@@ -5,8 +5,8 @@ use gpui::{
InspectorElementId, IntoElement, LayoutId, Pixels, Point, Position, Style, Window, size,
};
/// What was raised while a layer prepaints its content, each with where it sits and its priority.
type Queue = Vec<(AnyElement, Point<Pixels>, usize)>;
/// What was raised while a layer prepaints its content, each by its owner's id, with where it sits and its priority.
type Queue = Vec<(ElementId, AnyElement, Point<Pixels>, usize)>;
thread_local! {
static LAYERS: RefCell<Vec<Queue>> = const { RefCell::new(Vec::new()) };
@@ -16,12 +16,13 @@ fn nested() -> bool {
LAYERS.with_borrow(|layers| !layers.is_empty())
}
/// Draws `child` over the page, as gpui's `deferred` does. gpui cannot defer a draw inside a deferred one, so what is raised inside something raised is laid out and drawn last within it: a list opened in a dialog lies over the dialog.
pub fn raise(child: impl IntoElement) -> Raised {
/// Draws `child` over the page, as gpui's `deferred` does. gpui cannot defer a draw inside a deferred one, so what is raised inside something raised is laid out and drawn last within it, under `id`: a list opened in a dialog lies over the dialog and keeps its state.
pub fn raise(id: impl Into<ElementId>, child: impl IntoElement) -> Raised {
let layer = Layer {
child: child.into_any_element(),
};
Raised {
id: id.into(),
child: Some(layer.into_any_element()),
priority: 0,
nested: false,
@@ -30,6 +31,7 @@ pub fn raise(child: impl IntoElement) -> Raised {
/// An element drawn over the page; see [`raise`].
pub struct Raised {
id: ElementId,
child: Option<AnyElement>,
priority: usize,
nested: bool,
@@ -100,7 +102,7 @@ impl Element for Raised {
let queue = layers
.last_mut()
.expect("a nested element prepaints in its layer");
queue.push((child, bounds.origin, self.priority));
queue.push((self.id.clone(), child, bounds.origin, self.priority));
});
}
@@ -141,7 +143,7 @@ fn collect(f: impl FnOnce()) -> Queue {
impl Element for Layer {
type RequestLayoutState = ();
type PrepaintState = Vec<AnyElement>;
type PrepaintState = Vec<(ElementId, AnyElement)>;
fn id(&self) -> Option<ElementId> {
None
@@ -171,11 +173,11 @@ impl Element for Layer {
_: &mut (),
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
) -> Vec<(ElementId, AnyElement)> {
let mut raised = collect(|| {
self.child.prepaint(window, cx);
});
raised.sort_by_key(|(_, _, priority)| *priority);
raised.sort_by_key(|(_, _, _, priority)| *priority);
let viewport = window.viewport_size();
let space = size(
AvailableSpace::Definite(viewport.width),
@@ -183,12 +185,11 @@ impl Element for Layer {
);
raised
.into_iter()
.enumerate()
.map(|(ix, (mut element, at, _))| {
window.with_element_namespace(("raised", ix), |window| {
.map(|(id, mut element, at, _)| {
window.with_element_namespace(id.clone(), |window| {
element.prepaint_as_root(at, space, window, cx)
});
element
(id, element)
})
.collect()
}
@@ -199,13 +200,13 @@ impl Element for Layer {
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut (),
raised: &mut Vec<AnyElement>,
raised: &mut Vec<(ElementId, AnyElement)>,
window: &mut Window,
cx: &mut App,
) {
self.child.paint(window, cx);
for (ix, element) in raised.iter_mut().enumerate() {
window.with_element_namespace(("raised", ix), |window| element.paint(window, cx));
for (id, element) in raised.iter_mut() {
window.with_element_namespace(id.clone(), |window| element.paint(window, cx));
}
}
}
+81 -2
View File
@@ -1,6 +1,8 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{
Context, IntoElement, Modifiers, ParentElement, Render, Styled, TestAppContext, Window,
anchored, div, point, prelude::*, px,
App, Context, Entity, FocusHandle, IntoElement, Modifiers, ParentElement, Render, RenderOnce,
Styled, TestAppContext, Window, anchored, div, point, prelude::*, px,
};
use super::raise;
@@ -27,11 +29,13 @@ impl Render for Stack {
.size(px(200.0))
.on_click(move |_, _, cx| cover.update(cx, |stack, _| stack.heard.push("cover")));
div().size_full().child(raise(
"sheet",
anchored().position(point(px(0.0), px(0.0))).child(
div()
.relative()
.size(px(200.0))
.child(raise(
"button",
anchored().position(point(px(50.0), px(50.0))).child(button),
))
.child(sheet),
@@ -51,3 +55,78 @@ fn what_is_raised_inside_a_raise_lies_over_it(cx: &mut TestAppContext) {
["inner"]
);
}
/// A focusable box whose focus handle lives in keyed state; each draw tells the test which one it drew.
#[derive(IntoElement)]
struct Held {
seen: Rc<RefCell<Vec<Entity<FocusHandle>>>>,
}
impl RenderOnce for Held {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let kept = window.use_keyed_state("kept", cx, |_, cx| cx.focus_handle());
self.seen.borrow_mut().push(kept.clone());
let handle = kept.read(cx).clone();
div().id("held").size(px(40.0)).track_focus(&handle)
}
}
/// A raised sheet holding a raised tip that comes and goes, then a raised box that must stay itself.
struct Nest {
tip: bool,
seen: Rc<RefCell<Vec<Entity<FocusHandle>>>>,
}
impl Render for Nest {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let tip = self.tip.then(|| {
raise(
"tip",
anchored()
.position(point(px(0.0), px(0.0)))
.child(div().child("tip")),
)
});
let held = Held {
seen: self.seen.clone(),
};
div().size_full().child(raise(
"sheet",
anchored().position(point(px(0.0), px(0.0))).child(
div().size(px(200.0)).children(tip).child(raise(
"held",
anchored().position(point(px(50.0), px(50.0))).child(held),
)),
),
))
}
}
#[gpui::test]
fn a_raise_keeps_its_state_while_another_comes_before_it(cx: &mut TestAppContext) {
cx.update(Theme::init);
let seen = Rc::new(RefCell::new(Vec::new()));
let (view, cx) = cx.add_window_view(|_, _| Nest {
tip: false,
seen: seen.clone(),
});
cx.run_until_parked();
let first = seen.borrow().last().cloned().expect("the box drew");
let handle = first.read_with(cx, |handle, _| handle.clone());
cx.update(|window, _| window.focus(&handle));
view.update(cx, |nest, cx| {
nest.tip = true;
cx.notify();
});
cx.run_until_parked();
let last = seen.borrow().last().cloned().expect("the box drew again");
assert_eq!(
last.entity_id(),
first.entity_id(),
"the box kept its state"
);
assert!(
cx.update(|window, _| handle.is_focused(window)),
"the box kept focus"
);
}
+2
View File
@@ -139,6 +139,7 @@ impl RenderOnce for TooltipTrigger {
let (delay, follow) = (self.delay, self.follow);
let content = self.content;
let id = self.id.clone();
div()
.id(self.id)
.child(self.child)
@@ -177,6 +178,7 @@ impl RenderOnce for TooltipTrigger {
let body = content(window, cx);
let below = cx.theme().cursor_offset().to_pixels(window.rem_size());
trigger.child(raise(
(id, "raised"),
anchored()
.position(mouse + point(Pixels::ZERO, below))
.snap_to_window()