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:
1 parent
5b0a8443a7
commit
9abab5f2ba
20 files changed
+405
-49
No files matched your search
@@ -6,15 +6,15 @@ use std::{
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, ClickEvent, Context, Corner, ElementId, Entity,
|
||||
FontWeight, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, Task, Window, anchored, canvas, deferred, div, point,
|
||||
prelude::*, relative,
|
||||
StatefulInteractiveElement, Styled, Task, Window, anchored, canvas, div, point, prelude::*,
|
||||
relative,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant, IconButton},
|
||||
forms::Run,
|
||||
motion,
|
||||
primitives::{Icon, IconName, Severity},
|
||||
primitives::{Icon, IconName, Severity, raise},
|
||||
theme::{ActiveTheme, ControlSize, Elevation, IconSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
@@ -234,7 +234,7 @@ impl RenderOnce for ToastViewport {
|
||||
hold.update(cx, |toaster, cx| toaster.hold(*hovered, cx))
|
||||
})
|
||||
.children(cards);
|
||||
deferred(
|
||||
raise(
|
||||
anchored()
|
||||
.position(point(viewport.width - margin, viewport.height - margin))
|
||||
.anchor(Corner::BottomRight)
|
||||
|
||||
@@ -3,13 +3,13 @@ use std::rc::Rc;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, Bounds, Corner, Div, ElementId, Entity,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle,
|
||||
SharedString, Stateful, StatefulInteractiveElement, Styled, Window, anchored, canvas, deferred,
|
||||
div, prelude::*,
|
||||
SharedString, Stateful, StatefulInteractiveElement, Styled, Window, anchored, canvas, div,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
motion,
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, raise},
|
||||
theme::{ActiveTheme, ControlSize, Elevation, IconSize, Radius, TextSize},
|
||||
typography::Ellipsis,
|
||||
};
|
||||
@@ -220,7 +220,7 @@ pub(crate) fn float_height(
|
||||
}),
|
||||
)
|
||||
};
|
||||
deferred(placed.snap_to_window())
|
||||
raise(placed.snap_to_window())
|
||||
.with_priority(1)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@ use std::{cell::Cell, rc::Rc};
|
||||
use gpui::{
|
||||
AnyElement, App, DragMoveEvent, ElementId, EmptyView, EntityId, FontWeight, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Point, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Window, anchored, deferred, div, point,
|
||||
prelude::*,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Window, anchored, div, point, prelude::*,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::seeded::use_seeded;
|
||||
use crate::theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize};
|
||||
use crate::{
|
||||
primitives::raise,
|
||||
theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize},
|
||||
};
|
||||
|
||||
struct Carry {
|
||||
owner: EntityId,
|
||||
@@ -147,7 +149,7 @@ impl RenderOnce for FloatingPanel {
|
||||
.child(self.title),
|
||||
)
|
||||
.child(div().p_3().children(self.body));
|
||||
deferred(anchored().position(at).child(panel))
|
||||
raise(anchored().position(at).child(panel))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+34
-2
@@ -2,12 +2,13 @@ use gpui::{
|
||||
Context, Entity, IntoElement, Modifiers, ParentElement, Render, SharedString, Styled,
|
||||
TestAppContext, VisualTestContext, Window, div, px,
|
||||
};
|
||||
use jiff::{Timestamp, tz::TimeZone};
|
||||
use jiff::{Timestamp, ToSpan, tz::TimeZone};
|
||||
|
||||
use super::{Mailing, heard, mailing, note, press, settle, setup, tab_to};
|
||||
use crate::{
|
||||
forms,
|
||||
mail::{Label, LabelPicker, SnoozePicker, snooze::snoozes},
|
||||
mail::{Label, LabelPicker, SnoozePicker, snooze::snoozes, times::at_hour},
|
||||
typography::format::system_zone,
|
||||
};
|
||||
|
||||
/// Labels the host keeps, the keys on, and what it heard.
|
||||
@@ -166,3 +167,34 @@ fn the_last_row_asks_for_a_time_of_ones_own(cx: &mut TestAppContext) {
|
||||
assert!(cx.debug_bounds("time-dialog").is_some(), "the dialog shows");
|
||||
assert!(heard(&host, cx).is_empty(), "no time yet");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_dialog_takes_only_a_time_ahead_of_now(cx: &mut TestAppContext) {
|
||||
let (host, cx) = mailing(snoozing, cx);
|
||||
tab_to(1, cx);
|
||||
for key in [
|
||||
"enter", "up", "enter", "tab", "enter", "left", "enter", "escape",
|
||||
] {
|
||||
press(key, cx);
|
||||
}
|
||||
assert!(
|
||||
cx.debug_bounds("time-late").is_some(),
|
||||
"yesterday has passed"
|
||||
);
|
||||
for key in ["tab", "tab", "enter"] {
|
||||
press(key, cx);
|
||||
}
|
||||
assert!(heard(&host, cx).is_empty(), "Snooze waits for a time ahead");
|
||||
for key in [
|
||||
"right", "right", "right", "enter", "escape", "tab", "tab", "enter",
|
||||
] {
|
||||
press(key, cx);
|
||||
}
|
||||
let today = Timestamp::now().to_zoned(system_zone("a test")).date();
|
||||
let ahead = at_hour(
|
||||
today.checked_add(2.days()).expect("a day"),
|
||||
0,
|
||||
&TimeZone::UTC,
|
||||
);
|
||||
assert_eq!(heard(&host, cx), [format!("until {ahead}")]);
|
||||
}
|
||||
+53
-9
@@ -40,6 +40,15 @@ pub(super) fn named(name: &str, at: Timestamp, zone: &TimeZone) -> (String, Time
|
||||
(format!("{name} · {hour}"), at)
|
||||
}
|
||||
|
||||
/// The moment `chosen` names in `zone`, once it lies after `now`.
|
||||
pub(super) fn ahead(chosen: DateTime, zone: &TimeZone, now: Timestamp) -> Option<Timestamp> {
|
||||
let at = chosen
|
||||
.to_zoned(zone.clone())
|
||||
.expect("the zone holds the time")
|
||||
.timestamp();
|
||||
(at > now).then_some(at)
|
||||
}
|
||||
|
||||
/// `times` less each moment an earlier one names, as tomorrow names next Monday on a Sunday.
|
||||
pub(super) fn distinct(
|
||||
times: impl IntoIterator<Item = (String, Timestamp)>,
|
||||
@@ -99,16 +108,16 @@ pub(super) fn time_dialog(
|
||||
return None;
|
||||
}
|
||||
let chosen = now.chosen;
|
||||
let at = chosen.map(|chosen| {
|
||||
chosen
|
||||
.to_zoned(zone.clone())
|
||||
.expect("the zone holds the time")
|
||||
.timestamp()
|
||||
});
|
||||
let ahead = at.filter(|at| *at > Timestamp::now());
|
||||
let ahead = chosen.and_then(|chosen| ahead(chosen, zone, Timestamp::now()));
|
||||
let (closing, choosing, id) = (picking.clone(), picking.clone(), id.clone());
|
||||
let late = (at.is_some() && ahead.is_none())
|
||||
.then(|| FormError::new((id.clone(), "late"), "That time has passed."));
|
||||
let late = (chosen.is_some() && ahead.is_none()).then(|| {
|
||||
div()
|
||||
.debug_selector(|| "time-late".into())
|
||||
.child(FormError::new(
|
||||
(id.clone(), "late"),
|
||||
"That time has passed.",
|
||||
))
|
||||
});
|
||||
let dialog = Dialog::new((id.clone(), "dialog"), ask.title, move |_, cx| {
|
||||
closing.update(cx, |picking, cx| {
|
||||
*picking = Picking::default();
|
||||
@@ -155,3 +164,38 @@ pub(super) fn time_dialog(
|
||||
});
|
||||
Some(dialog)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use jiff::{
|
||||
SignedDuration,
|
||||
civil::date,
|
||||
tz::{TimeZone, offset},
|
||||
};
|
||||
|
||||
use super::{ahead, at_hour};
|
||||
|
||||
#[test]
|
||||
fn a_picked_time_counts_once_it_lies_ahead_of_now() {
|
||||
let day = date(2026, 9, 27);
|
||||
let now = at_hour(day, 9, &TimeZone::UTC);
|
||||
let utc = &TimeZone::UTC;
|
||||
assert_eq!(
|
||||
ahead(day.at(8, 59, 0, 0), utc, now),
|
||||
None,
|
||||
"a minute behind"
|
||||
);
|
||||
assert_eq!(ahead(day.at(9, 0, 0, 0), utc, now), None, "now itself");
|
||||
assert_eq!(
|
||||
ahead(day.at(9, 1, 0, 0), utc, now),
|
||||
Some(now + SignedDuration::from_mins(1)),
|
||||
"a minute ahead"
|
||||
);
|
||||
let tokyo = TimeZone::fixed(offset(9));
|
||||
assert_eq!(
|
||||
ahead(day.at(10, 0, 0, 0), &tokyo, now),
|
||||
None,
|
||||
"10:00 in Tokyo is 01:00 here"
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -2,8 +2,7 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, Bounds, ElementId, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Point, Styled, Window, anchored, canvas, deferred, div,
|
||||
prelude::*,
|
||||
IntoElement, ParentElement, Pixels, Point, Styled, Window, anchored, canvas, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -13,7 +12,7 @@ use super::{
|
||||
use crate::{
|
||||
forms::{Down, Enter, Run, TextInput, Up, float},
|
||||
motion,
|
||||
primitives::{Icon, IconName, Takeover, give_back, take_focus},
|
||||
primitives::{Icon, IconName, Takeover, give_back, raise, take_focus},
|
||||
theme::{ActiveTheme, IconSize},
|
||||
};
|
||||
|
||||
@@ -384,7 +383,7 @@ pub(crate) fn hang(
|
||||
});
|
||||
let placed = match spot {
|
||||
Spot::Under => float(state.read(cx).anchor, menu.entries.len(), root, window, cx),
|
||||
Spot::At(point) => deferred(anchored().position(point).snap_to_window().child(
|
||||
Spot::At(point) => raise(anchored().position(point).snap_to_window().child(
|
||||
root.with_animation((id.clone(), "in"), enter.clone(), |root, t| root.opacity(t)),
|
||||
))
|
||||
.with_priority(1)
|
||||
@@ -393,7 +392,7 @@ pub(crate) fn hang(
|
||||
let subs = (1..depth).filter_map(|level| {
|
||||
let row = state.read(cx).levels[level - 1].row;
|
||||
(row != Bounds::default()).then(|| {
|
||||
deferred(
|
||||
raise(
|
||||
anchored().position(row.top_right()).snap_to_window().child(
|
||||
draw::panel(id, menu, state, level, &close, None, cx)
|
||||
.mt_neg_1()
|
||||
|
||||
+3
-3
@@ -3,14 +3,14 @@ use std::{f32::consts::TAU, rc::Rc};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, Bounds, ElementId, Entity, FontWeight,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce,
|
||||
SharedString, Styled, Window, anchored, canvas, deferred, div, point,
|
||||
SharedString, Styled, Window, anchored, canvas, div, point,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
forms::{Pick, Run},
|
||||
motion,
|
||||
primitives::{Icon, IconName, give_back, take_focus},
|
||||
primitives::{Icon, IconName, give_back, raise, take_focus},
|
||||
theme::{ActiveTheme, Elevation, IconSize, TextSize},
|
||||
};
|
||||
|
||||
@@ -302,7 +302,7 @@ impl RenderOnce for PieMenu {
|
||||
}
|
||||
});
|
||||
region.child(
|
||||
deferred(
|
||||
raise(
|
||||
anchored()
|
||||
.position(point(Pixels::ZERO, Pixels::ZERO))
|
||||
.child(
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, ElementId, FocusHandle, FontWeight,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, Styled,
|
||||
Window, anchored, deferred, div, point, prelude::*,
|
||||
Window, anchored, div, point, prelude::*,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
forms::{Enter, Run},
|
||||
motion,
|
||||
primitives::{Backdrop, FocusScope, IconName, give_back, take_focus},
|
||||
primitives::{Backdrop, FocusScope, IconName, give_back, raise, take_focus},
|
||||
theme::{ActiveTheme, Elevation, Radius, TextSize},
|
||||
};
|
||||
|
||||
@@ -270,7 +270,7 @@ impl RenderOnce for Dialog {
|
||||
if fullscreen {
|
||||
let viewport = window.viewport_size();
|
||||
let top = cx.theme().titlebar_height().to_pixels(window.rem_size());
|
||||
return deferred(
|
||||
return raise(
|
||||
anchored().position(point(Pixels::ZERO, top)).child(
|
||||
div()
|
||||
.w(viewport.width)
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnimationExt, AnyElement, App, Corner, ElementId, Entity, FontWeight, InteractiveElement,
|
||||
IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, anchored, canvas,
|
||||
deferred, div, point, prelude::*,
|
||||
IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, anchored, canvas, div,
|
||||
point, prelude::*,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
forms::{Run, TextInput, surface},
|
||||
layout::Collapsible,
|
||||
motion,
|
||||
primitives::IconName,
|
||||
primitives::{IconName, raise},
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ impl RenderOnce for FloatingToolbar {
|
||||
.when_some(anchor, |host, anchor| {
|
||||
let lift = cx.theme().float_gap().to_pixels(window.rem_size());
|
||||
host.child(
|
||||
deferred(
|
||||
raise(
|
||||
anchored()
|
||||
.position(anchor.origin - point(gpui::Pixels::ZERO, lift))
|
||||
.anchor(Corner::BottomLeft)
|
||||
|
||||
@@ -3,14 +3,14 @@ use std::rc::Rc;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, Div, ElementId, ImageSource, InteractiveElement, IntoElement,
|
||||
MouseButton, ObjectFit, ParentElement, Pixels, Point, RenderOnce, SharedString, Size, Stateful,
|
||||
StatefulInteractiveElement, Styled, Window, anchored, canvas, deferred, div, prelude::*,
|
||||
StatefulInteractiveElement, Styled, Window, anchored, canvas, div, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::label_size,
|
||||
forms::Run,
|
||||
motion,
|
||||
primitives::{FocusRing, FocusScope, Icon, IconName, Image, give_back, take_focus},
|
||||
primitives::{FocusRing, FocusScope, Icon, IconName, Image, give_back, raise, take_focus},
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
};
|
||||
|
||||
@@ -291,7 +291,7 @@ impl RenderOnce for Lightbox {
|
||||
),
|
||||
),
|
||||
);
|
||||
deferred(anchored().position(Point::default()).child(stage))
|
||||
raise(anchored().position(Point::default()).child(stage))
|
||||
.with_priority(1)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ use std::{cell::RefCell, rc::Rc};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, Bounds, ElementId, FontWeight, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, Subscription,
|
||||
Window, anchored, canvas, deferred, div, point, prelude::*, size,
|
||||
Window, anchored, canvas, div, point, prelude::*, size,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
forms::{Run, float_height, surface},
|
||||
motion,
|
||||
primitives::{FocusScope, give_back, take_focus},
|
||||
primitives::{FocusScope, give_back, raise, take_focus},
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
};
|
||||
|
||||
@@ -192,7 +192,7 @@ fn light(
|
||||
);
|
||||
div()
|
||||
.child(
|
||||
deferred(
|
||||
raise(
|
||||
anchored()
|
||||
.position(Point::default())
|
||||
.child(div().w(viewport.width).h(viewport.height).child(frame)),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
Reference in new issue
Block a user