Primitives · raise: what opens inside a dialog lies over it

gpui 0.2.2 asserts on a deferred draw inside another, and every dialog,
sheet and popover draws inside one, so opening a date field in the
snooze or schedule dialog panicked, as would any list, menu or picker
opened in an overlay. primitives::raise replaces each deferred draw:
at the top it defers as before; inside something raised it leaves a
placeholder in the tree, and its layer lays it out on its own, then
draws it last. check.sh refuses gpui's deferred anywhere else.

The time dialog's rule is now times::ahead, tested for a moment
behind, now itself, a moment ahead and another zone; a gpui test picks
yesterday in the snooze dialog, sees that it has passed and that
nothing is sent, then picks two days ahead and hears that moment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 01:51:51 -04:00
1 parent 5b0a8443a7
commit 9abab5f2ba
20 files changed
+405 -49

No files matched your search

+3 -3
View File
@@ -2,11 +2,11 @@ use std::rc::Rc;
use gpui::{
Animation, AnimationExt, AnyElement, App, ElementId, InteractiveElement, IntoElement,
MouseButton, ParentElement, Point, RenderOnce, Styled, Window, anchored, deferred, div,
prelude::*,
MouseButton, ParentElement, Point, RenderOnce, Styled, Window, anchored, div, prelude::*,
};
use smallvec::SmallVec;
use super::raise;
use crate::{motion, theme::ActiveTheme};
type Dismiss = Rc<dyn Fn(&mut Window, &mut App)>;
@@ -81,7 +81,7 @@ impl RenderOnce for Backdrop {
|scrim, t| scrim.opacity(t),
);
deferred(
raise(
anchored().position(Point::default()).child(
div()
.relative()
+211
View File
@@ -0,0 +1,211 @@
use std::{cell::RefCell, panic::Location};
use gpui::{
AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId,
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)>;
thread_local! {
static LAYERS: RefCell<Vec<Queue>> = const { RefCell::new(Vec::new()) };
}
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 {
let layer = Layer {
child: child.into_any_element(),
};
Raised {
child: Some(layer.into_any_element()),
priority: 0,
nested: false,
}
}
/// An element drawn over the page; see [`raise`].
pub struct Raised {
child: Option<AnyElement>,
priority: usize,
nested: bool,
}
impl Raised {
/// A higher priority draws later, over the lower.
pub fn with_priority(mut self, priority: usize) -> Self {
self.priority = priority;
self
}
}
impl IntoElement for Raised {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for Raised {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
self.nested = nested();
if self.nested {
let style = Style {
position: Position::Absolute,
..Style::default()
};
return (window.request_layout(style, [], cx), ());
}
let child = self.child.as_mut().expect("a raised element lays out once");
(child.request_layout(window, cx), ())
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut (),
window: &mut Window,
_: &mut App,
) {
let child = self.child.take().expect("a raised element prepaints once");
if !self.nested {
window.defer_draw(child, window.element_offset(), self.priority);
return;
}
LAYERS.with_borrow_mut(|layers| {
let queue = layers
.last_mut()
.expect("a nested element prepaints in its layer");
queue.push((child, bounds.origin, self.priority));
});
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut (),
_: &mut (),
_: &mut Window,
_: &mut App,
) {
}
}
/// What a raised element draws: its content, then what was raised inside it, each laid out on its own and the higher priority last.
struct Layer {
child: AnyElement,
}
impl IntoElement for Layer {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// Runs `f` with a fresh queue on the stack, and hands back what was raised meanwhile.
fn collect(f: impl FnOnce()) -> Queue {
LAYERS.with_borrow_mut(|layers| layers.push(Vec::new()));
f();
LAYERS
.with_borrow_mut(|layers| layers.pop())
.expect("a layer pops what it pushed")
}
impl Element for Layer {
type RequestLayoutState = ();
type PrepaintState = Vec<AnyElement>;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
let mut layout = None;
collect(|| layout = Some(self.child.request_layout(window, cx)));
(layout.expect("the child laid out"), ())
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut (),
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
let mut raised = collect(|| {
self.child.prepaint(window, cx);
});
raised.sort_by_key(|(_, _, priority)| *priority);
let viewport = window.viewport_size();
let space = size(
AvailableSpace::Definite(viewport.width),
AvailableSpace::Definite(viewport.height),
);
raised
.into_iter()
.enumerate()
.map(|(ix, (mut element, at, _))| {
window.with_element_namespace(("raised", ix), |window| {
element.prepaint_as_root(at, space, window, cx)
});
element
})
.collect()
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut (),
raised: &mut Vec<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));
}
}
}
+5
View File
@@ -6,11 +6,15 @@ mod focus;
mod ghost;
mod icon;
mod image;
mod layer;
mod measure;
mod pressable;
mod severity;
mod tooltip;
#[cfg(all(test, feature = "test-support"))]
mod tests;
pub use backdrop::{Backdrop, Place};
pub use disclosure::Disclosure;
pub use divider::Divider;
@@ -21,6 +25,7 @@ pub use ghost::DragGhost;
pub use icon::{Icon, IconName};
pub use image::Image;
pub(crate) use image::{checked_ratio, framed};
pub use layer::{Raised, raise};
pub use measure::{IntersectionObserver, Measure};
pub use pressable::Pressable;
pub use severity::Severity;
+53
View File
@@ -0,0 +1,53 @@
use gpui::{
Context, IntoElement, Modifiers, ParentElement, Render, Styled, TestAppContext, Window,
anchored, div, point, prelude::*, px,
};
use super::raise;
use crate::theme::Theme;
/// A raised square holding a raised button, under a cover drawn after it in the same layer.
struct Stack {
heard: Vec<&'static str>,
}
impl Render for Stack {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (inner, cover) = (cx.entity(), cx.entity());
let button = div()
.id("inner")
.size(px(100.0))
.occlude()
.on_click(move |_, _, cx| inner.update(cx, |stack, _| stack.heard.push("inner")));
let sheet = div()
.id("cover")
.absolute()
.top_0()
.left_0()
.size(px(200.0))
.on_click(move |_, _, cx| cover.update(cx, |stack, _| stack.heard.push("cover")));
div().size_full().child(raise(
anchored().position(point(px(0.0), px(0.0))).child(
div()
.relative()
.size(px(200.0))
.child(raise(
anchored().position(point(px(50.0), px(50.0))).child(button),
))
.child(sheet),
),
))
}
}
#[gpui::test]
fn what_is_raised_inside_a_raise_lies_over_it(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (view, cx) = cx.add_window_view(|_, _| Stack { heard: Vec::new() });
cx.run_until_parked();
cx.simulate_click(point(px(100.0), px(100.0)), Modifiers::none());
assert_eq!(
view.read_with(cx, |stack, _| stack.heard.clone()),
["inner"]
);
}
+3 -2
View File
@@ -3,10 +3,11 @@ use std::{rc::Rc, time::Duration};
use gpui::{
Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Context, Div, ElementId,
InteractiveElement, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Task, Window, anchored, deferred, div, point,
SharedString, StatefulInteractiveElement, Styled, Task, Window, anchored, div, point,
prelude::*,
};
use super::raise;
use crate::{
motion,
theme::{ActiveTheme, Elevation, Radius, TextSize},
@@ -175,7 +176,7 @@ impl RenderOnce for TooltipTrigger {
.when(visible, |trigger| {
let body = content(window, cx);
let below = cx.theme().cursor_offset().to_pixels(window.rem_size());
trigger.child(deferred(
trigger.child(raise(
anchored()
.position(mouse + point(Pixels::ZERO, below))
.snap_to_window()