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:
1 parent
341420d1e4
commit
51ade29032
25 files changed
+188
-40
No files matched your search
@@ -198,7 +198,9 @@ impl RenderOnce for LinkEditor {
|
||||
}),
|
||||
),
|
||||
);
|
||||
div().id(self.id).child(float(anchor, 1, panel, window, cx))
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.child(float(self.id, anchor, 1, panel, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ impl RenderOnce for ToastViewport {
|
||||
cards.push(card(&self.id, ix, &self.toaster, window, cx));
|
||||
}
|
||||
let stack = div()
|
||||
.id(self.id)
|
||||
.id(self.id.clone())
|
||||
.occlude()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -235,6 +235,7 @@ impl RenderOnce for ToastViewport {
|
||||
})
|
||||
.children(cards);
|
||||
raise(
|
||||
(self.id, "raised"),
|
||||
anchored()
|
||||
.position(point(viewport.width - margin, viewport.height - margin))
|
||||
.anchor(Corner::BottomRight)
|
||||
|
||||
@@ -265,6 +265,7 @@ impl RenderOnce for Cascader {
|
||||
});
|
||||
let close = state.clone();
|
||||
float(
|
||||
self.id.clone(),
|
||||
anchor,
|
||||
deepest,
|
||||
surface((self.id.clone(), "columns"), cx)
|
||||
|
||||
@@ -228,6 +228,7 @@ fn period_picker(
|
||||
};
|
||||
let grid = period_grid(&key, build(shown), grid, window, cx);
|
||||
float(
|
||||
key.clone(),
|
||||
anchor,
|
||||
6,
|
||||
surface((key.clone(), "popup"), cx)
|
||||
|
||||
@@ -246,6 +246,7 @@ impl RenderOnce for DatePicker {
|
||||
}
|
||||
let (escape, done) = (close.clone(), close.clone());
|
||||
float(
|
||||
id.clone(),
|
||||
anchor,
|
||||
CALENDAR_ROWS,
|
||||
surface((id.clone(), "popup"), cx)
|
||||
@@ -347,6 +348,7 @@ impl RenderOnce for DateRangePicker {
|
||||
}
|
||||
let (hover, pick, escape) = (state.clone(), state.clone(), close.clone());
|
||||
float(
|
||||
id.clone(),
|
||||
anchor,
|
||||
CALENDAR_ROWS,
|
||||
surface((id.clone(), "popup"), cx)
|
||||
|
||||
@@ -256,6 +256,7 @@ impl RenderOnce for TimePicker {
|
||||
let columns = time_columns(&id, time, step, commit.clone(), window, cx);
|
||||
let out = close.clone();
|
||||
float(
|
||||
id.clone(),
|
||||
anchor,
|
||||
CALENDAR_ROWS,
|
||||
surface((id.clone(), "popup"), cx)
|
||||
@@ -368,6 +369,7 @@ impl RenderOnce for DateTimePicker {
|
||||
);
|
||||
let out = close.clone();
|
||||
float(
|
||||
id.clone(),
|
||||
anchor,
|
||||
CALENDAR_ROWS,
|
||||
surface((id.clone(), "popup"), cx)
|
||||
|
||||
@@ -77,6 +77,7 @@ impl RenderOnce for WeekPicker {
|
||||
}
|
||||
let (escape, out, on_change) = (close.clone(), close.clone(), on_change.clone());
|
||||
float(
|
||||
id.clone(),
|
||||
anchor,
|
||||
CALENDAR_ROWS,
|
||||
surface((id.clone(), "popup"), cx)
|
||||
|
||||
@@ -166,8 +166,9 @@ pub(crate) fn surface(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
}
|
||||
|
||||
/// Floats `content` of about `rows` rows under `anchor`, or over it when only above has room.
|
||||
/// Floats `content` of about `rows` rows under `anchor`, or over it when only above has room; `id` is its owner's.
|
||||
pub(crate) fn float(
|
||||
id: impl Into<ElementId>,
|
||||
anchor: Bounds<Pixels>,
|
||||
rows: usize,
|
||||
content: impl IntoElement,
|
||||
@@ -178,7 +179,7 @@ pub(crate) fn float(
|
||||
let rem = window.rem_size();
|
||||
let row = theme.control_height(ControlSize::Md).to_pixels(rem);
|
||||
let height = (row * (rows + 1) as f32).min(theme.list_max_height().to_pixels(rem));
|
||||
float_height(anchor, height, content, window, cx)
|
||||
float_height(id, anchor, height, content, window, cx)
|
||||
}
|
||||
|
||||
/// Whether `height` goes over `anchor`: it does not fit below, and above has more room.
|
||||
@@ -187,8 +188,9 @@ pub(crate) fn opens_up(anchor: Bounds<Pixels>, height: Pixels, viewport: Pixels)
|
||||
height > below && anchor.top() > below
|
||||
}
|
||||
|
||||
/// Floats `content` of a known `height` under `anchor`, or over it when only above has room.
|
||||
/// Floats `content` of a known `height` under `anchor`, or over it when only above has room; `id` is its owner's.
|
||||
pub(crate) fn float_height(
|
||||
id: impl Into<ElementId>,
|
||||
anchor: Bounds<Pixels>,
|
||||
height: Pixels,
|
||||
content: impl IntoElement,
|
||||
@@ -220,7 +222,7 @@ pub(crate) fn float_height(
|
||||
}),
|
||||
)
|
||||
};
|
||||
raise(placed.snap_to_window())
|
||||
raise(id, placed.snap_to_window())
|
||||
.with_priority(1)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -298,7 +300,7 @@ impl Popup<'_> {
|
||||
})
|
||||
})
|
||||
});
|
||||
let list = surface(self.id, cx)
|
||||
let list = surface(self.id.clone(), cx)
|
||||
.debug_selector(|| "option-list".into())
|
||||
.min_w(theme.tooltip_max_width())
|
||||
.when(anchor.size.width > Pixels::ZERO, |list| {
|
||||
@@ -319,7 +321,7 @@ impl Popup<'_> {
|
||||
self.scroll.zip(self.reveal),
|
||||
|list, (handle, (ix, done))| list.child(revealer(handle, ix, done)),
|
||||
);
|
||||
float(anchor, self.rows.len(), list, window, cx)
|
||||
float(self.id, anchor, self.rows.len(), list, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ impl RenderOnce for FloatingPanel {
|
||||
};
|
||||
let panel = self
|
||||
.base
|
||||
.id(self.id)
|
||||
.id(self.id.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.overflow_hidden()
|
||||
@@ -149,7 +149,7 @@ impl RenderOnce for FloatingPanel {
|
||||
.child(self.title),
|
||||
)
|
||||
.child(div().p_3().children(self.body));
|
||||
raise(anchored().position(at).child(panel))
|
||||
raise((self.id, "raised"), anchored().position(at).child(panel))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-4
@@ -382,10 +382,25 @@ pub(crate) fn hang(
|
||||
}
|
||||
});
|
||||
let placed = match spot {
|
||||
Spot::Under => float(state.read(cx).anchor, menu.entries.len(), root, window, cx),
|
||||
Spot::At(point) => raise(anchored().position(point).snap_to_window().child(
|
||||
root.with_animation((id.clone(), "in"), enter.clone(), |root, t| root.opacity(t)),
|
||||
))
|
||||
Spot::Under => float(
|
||||
(id.clone(), "panel"),
|
||||
state.read(cx).anchor,
|
||||
menu.entries.len(),
|
||||
root,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
Spot::At(point) => raise(
|
||||
(id.clone(), "panel"),
|
||||
anchored()
|
||||
.position(point)
|
||||
.snap_to_window()
|
||||
.child(
|
||||
root.with_animation((id.clone(), "in"), enter.clone(), |root, t| {
|
||||
root.opacity(t)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.with_priority(1)
|
||||
.into_any_element(),
|
||||
};
|
||||
@@ -393,6 +408,7 @@ pub(crate) fn hang(
|
||||
let row = state.read(cx).levels[level - 1].row;
|
||||
(row != Bounds::default()).then(|| {
|
||||
raise(
|
||||
(id.clone(), format!("level-{level}")),
|
||||
anchored().position(row.top_right()).snap_to_window().child(
|
||||
draw::panel(id, menu, state, level, &close, None, cx)
|
||||
.mt_neg_1()
|
||||
|
||||
@@ -303,6 +303,7 @@ impl RenderOnce for PieMenu {
|
||||
});
|
||||
region.child(
|
||||
raise(
|
||||
(self.id.clone(), "raised"),
|
||||
anchored()
|
||||
.position(point(Pixels::ZERO, Pixels::ZERO))
|
||||
.child(
|
||||
|
||||
@@ -197,6 +197,7 @@ impl RenderOnce for StatusSetter {
|
||||
done(window, cx);
|
||||
});
|
||||
float_height(
|
||||
popup_id.clone(),
|
||||
anchor,
|
||||
height,
|
||||
surface((popup_id, "popup"), cx)
|
||||
|
||||
@@ -236,7 +236,14 @@ impl RenderOnce for NavigationMenu {
|
||||
.on_mouse_down_out(move |_, _, cx| show(&close, None, cx))
|
||||
.children(links);
|
||||
let rows = entries[entry].1.len().div_ceil(2) * 2;
|
||||
float(state.read(cx).anchors[entry], rows, content, window, cx)
|
||||
float(
|
||||
(self.id.clone(), format!("menu-{entry}")),
|
||||
state.read(cx).anchors[entry],
|
||||
rows,
|
||||
content,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (keys, key_entries, enter) = (state.clone(), entries.clone(), pick);
|
||||
div()
|
||||
|
||||
@@ -271,6 +271,7 @@ impl RenderOnce for Dialog {
|
||||
let viewport = window.viewport_size();
|
||||
let top = cx.theme().titlebar_height().to_pixels(window.rem_size());
|
||||
return raise(
|
||||
(self.id.clone(), "raised"),
|
||||
anchored().position(point(Pixels::ZERO, top)).child(
|
||||
div()
|
||||
.w(viewport.width)
|
||||
|
||||
@@ -69,6 +69,7 @@ impl RenderOnce for FloatingToolbar {
|
||||
let lift = cx.theme().float_gap().to_pixels(window.rem_size());
|
||||
host.child(
|
||||
raise(
|
||||
(self.id.clone(), "raised"),
|
||||
anchored()
|
||||
.position(anchor.origin - point(gpui::Pixels::ZERO, lift))
|
||||
.anchor(Corner::BottomLeft)
|
||||
|
||||
@@ -132,6 +132,13 @@ impl RenderOnce for HoverCard {
|
||||
.size_full(),
|
||||
);
|
||||
let (anchor, height) = (state.read(cx).host, state.read(cx).height);
|
||||
host.child(float_height(anchor, height, card, window, cx))
|
||||
host.child(float_height(
|
||||
self.id.clone(),
|
||||
anchor,
|
||||
height,
|
||||
card,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -291,8 +291,11 @@ impl RenderOnce for Lightbox {
|
||||
),
|
||||
),
|
||||
);
|
||||
raise(anchored().position(Point::default()).child(stage))
|
||||
.with_priority(1)
|
||||
.into_any_element()
|
||||
raise(
|
||||
(self.id.clone(), "raised"),
|
||||
anchored().position(Point::default()).child(stage),
|
||||
)
|
||||
.with_priority(1)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,13 @@ impl RenderOnce for Popover {
|
||||
.left_0()
|
||||
.size_full(),
|
||||
);
|
||||
host.child(float_height(anchor, height, panel, window, cx))
|
||||
host.child(float_height(
|
||||
self.id.clone(),
|
||||
anchor,
|
||||
height,
|
||||
panel,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -193,13 +193,21 @@ fn light(
|
||||
div()
|
||||
.child(
|
||||
raise(
|
||||
(id.clone(), "raised"),
|
||||
anchored()
|
||||
.position(Point::default())
|
||||
.child(div().w(viewport.width).h(viewport.height).child(frame)),
|
||||
)
|
||||
.with_priority(1),
|
||||
)
|
||||
.child(float_height(target, card_height, card, window, cx))
|
||||
.child(float_height(
|
||||
(id.clone(), "card"),
|
||||
target,
|
||||
card_height,
|
||||
card,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user