mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 14:14:13 +00:00
UI.8: cancel renderer-owned resize gestures on workspace replacement
This commit is contained in:
@@ -133,13 +133,22 @@ pub fn begin_drag<P: Clone + Send + Sync + 'static>(ctx: &Context, area_id: Id,
|
||||
true
|
||||
}
|
||||
|
||||
/// Discard all pending gestures before replacing a workspace or its layout. The host owns
|
||||
/// the replacement snapshot, so this does not restore a group's previous rectangle.
|
||||
/// Discard pending tab/group gestures and an actively dragged renderer-owned control before
|
||||
/// replacing a workspace. Body/custom-header/menu gestures and other areas are untouched.
|
||||
/// Ownership is recorded as controls render; before an initial render or drag threshold there
|
||||
/// may be no active egui drag to cancel. Keyboard focus is unchanged. The host owns the
|
||||
/// replacement snapshot, so this does not restore a group's previous rectangle.
|
||||
pub fn cancel_drag<P: Clone + Send + Sync + 'static>(ctx: &Context, area_id: Id) {
|
||||
ctx.data_mut(|d| {
|
||||
let dragged = ctx.dragged_id();
|
||||
let owned = ctx.data_mut(|d| {
|
||||
d.remove::<Drag<P>>(area_id.with("drag"));
|
||||
d.remove::<GroupDrag<P>>(area_id.with("group-drag"));
|
||||
let ids = d.remove_temp::<Vec<Id>>(area_id.with("owned-interactions"));
|
||||
dragged.is_some_and(|id| ids.is_some_and(|ids| ids.contains(&id)))
|
||||
});
|
||||
if owned {
|
||||
ctx.stop_dragging();
|
||||
}
|
||||
}
|
||||
|
||||
/// One mutable application adapter owns bodies, menus and shaped text, avoiding competing
|
||||
@@ -301,6 +310,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<P> {
|
||||
blockers: self.blockers,
|
||||
layout,
|
||||
targets: Vec::new(),
|
||||
owned_interactions: Vec::new(),
|
||||
output: DockOutput::default(),
|
||||
};
|
||||
if let Some(root) = &layout.root {
|
||||
@@ -310,6 +320,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<P> {
|
||||
renderer.floating(ui.ctx());
|
||||
renderer.finish_group_drag(ui.ctx());
|
||||
renderer.finish_drag(ui.ctx());
|
||||
ui.ctx().data_mut(|d| d.insert_temp(self.id.with("owned-interactions"), renderer.owned_interactions));
|
||||
renderer.output
|
||||
}
|
||||
}
|
||||
@@ -325,10 +336,28 @@ struct Renderer<'a, P> {
|
||||
blockers: Vec<Rect>,
|
||||
layout: &'a Layout<P>,
|
||||
targets: Vec<DropTarget<P>>,
|
||||
owned_interactions: Vec<Id>,
|
||||
output: DockOutput<P>,
|
||||
}
|
||||
|
||||
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
fn register_interaction(&mut self, ctx: &Context, id: Id) {
|
||||
// A validated tree has at most 2*MAX_PANELS nodes. Tabs, stack dividers,
|
||||
// floating grips/corners and splits fit comfortably inside this fixed bound.
|
||||
const CAP: usize = 8 * super::MAX_PANELS;
|
||||
if self.owned_interactions.len() < CAP && !self.owned_interactions.contains(&id) {
|
||||
self.owned_interactions.push(id);
|
||||
}
|
||||
// Retain prior-frame controls until return, and publish current controls before
|
||||
// app callbacks. No Context lock spans a callback or a stop_dragging call.
|
||||
ctx.data_mut(|d| {
|
||||
let ids = d.get_temp_mut_or_default::<Vec<Id>>(self.id.with("owned-interactions"));
|
||||
if ids.len() < CAP && !ids.contains(&id) {
|
||||
ids.push(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn node(&mut self, ui: &mut Ui, node: &Node<P>, rect: Rect, path: &mut Vec<bool>) {
|
||||
if !super::targets::usable(rect) {
|
||||
return;
|
||||
@@ -356,6 +385,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
)
|
||||
.value(split.first_size(), lo..=hi)
|
||||
.show(ui);
|
||||
self.register_interaction(ui.ctx(), response.response.id);
|
||||
if response.delta != 0.0
|
||||
&& let Some(size) = split.resized(*size, (split.first_size() + response.delta).clamp(lo, hi) - split.first_size(), 0.0)
|
||||
{
|
||||
@@ -437,6 +467,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
)
|
||||
.value(first_size, lo..=hi)
|
||||
.show(ui);
|
||||
self.register_interaction(ui.ctx(), response.response.id);
|
||||
if response.delta != 0.0 && scale > 0.0 {
|
||||
let delta = f64::from((first_size + response.delta).clamp(lo, hi) - first_size) / scale;
|
||||
let next_a = (a + delta).clamp((a + b - 1_000_000.0).max(1.0), (a + b - 1.0).min(1_000_000.0));
|
||||
@@ -529,6 +560,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if floating {
|
||||
let grip = Rect::from_min_max(strip.min, pos2((strip.left() + 12.0).min(strip.right()), strip.bottom()));
|
||||
let response = ui.interact(grip, self.id.with(("move-group", panels.first())), Sense::drag()).on_hover_cursor(egui::CursorIcon::Grab);
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.move_label));
|
||||
if panels.iter().all(|p| (self.permissions)(p).movable)
|
||||
&& response.drag_started()
|
||||
@@ -648,6 +680,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
painter.rect_filled(rect, 0.0, if active || response.hovered() { self.style.active_background } else { self.style.tab_background });
|
||||
painter.galley(rect.center() - galley.size() * 0.5, galley.clone(), color);
|
||||
});
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
if response.clicked() {
|
||||
self.output.actions.push(Action::Activate { panel: panel.clone() });
|
||||
self.output.focused = Some(panel.clone());
|
||||
@@ -715,6 +748,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.group(ui, &group.panels, group.active, rect, true);
|
||||
let corner = Rect::from_min_max((rect.max - vec2(12.0, 12.0)).max(rect.min), rect.max);
|
||||
let response = ui.interact(corner, id.with("resize"), Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeNwSe);
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.resize_window_label));
|
||||
if response.drag_started() {
|
||||
response.request_focus();
|
||||
|
||||
@@ -924,3 +924,114 @@ fn chrome_focus_classifies_rendered_accordion_and_split_and_floating_resizers()
|
||||
assert!(DockArea::new(area).chrome_has_focus(&ctx, &layout));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_cancel_stops_owned_resize_controls_until_new_press() {
|
||||
let area = Id::new("test-area");
|
||||
let fixtures = [
|
||||
(sample(), area.with(("split", Vec::<bool>::new())), vec2(24.0, 0.0)),
|
||||
(
|
||||
Layout {
|
||||
root: Some(Node::Stack {
|
||||
entries: vec![StackEntry { panel: 1, open: true, height: Some(150.0) }, StackEntry { panel: 2, open: true, height: Some(150.0) }],
|
||||
}),
|
||||
floating: vec![],
|
||||
},
|
||||
area.with(("stack-resize", &1u32)),
|
||||
vec2(0.0, 24.0),
|
||||
),
|
||||
(sample(), area.with(("floating", &5u32)).with("resize"), vec2(24.0, 24.0)),
|
||||
];
|
||||
for (mut layout, id, delta) in fixtures {
|
||||
let ctx = Context::default();
|
||||
for _ in 0..3 {
|
||||
step(&ctx, &mut layout, vec![]);
|
||||
}
|
||||
let start = ctx.read_response(id).unwrap().rect.center();
|
||||
step(&ctx, &mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]);
|
||||
let actions = step(&ctx, &mut layout, vec![Event::PointerMoved(start + delta)]);
|
||||
assert!(!actions.is_empty(), "gesture fixture must actually resize: {id:?}");
|
||||
assert_eq!(ctx.dragged_id(), Some(id));
|
||||
// Keep all identities present in a replacement snapshot.
|
||||
let replacement = layout.clone();
|
||||
cancel_drag::<u32>(&ctx, area);
|
||||
assert_eq!(ctx.dragged_id(), None);
|
||||
for multiplier in [2.0, 3.0] {
|
||||
let actions = step(&ctx, &mut layout, vec![Event::PointerMoved(start + delta * multiplier)]);
|
||||
assert!(actions.is_empty(), "held old gesture produced {actions:?}");
|
||||
assert_eq!(layout, replacement);
|
||||
}
|
||||
step(&ctx, &mut layout, vec![pointer_button(start + delta * 3.0, false)]);
|
||||
for _ in 0..2 {
|
||||
step(&ctx, &mut layout, vec![]);
|
||||
}
|
||||
let start = ctx.read_response(id).unwrap().rect.center();
|
||||
step(&ctx, &mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]);
|
||||
assert!(!step(&ctx, &mut layout, vec![Event::PointerMoved(start + delta)]).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_cancel_preserves_other_area_and_body_drag_ownership() {
|
||||
for body_drag in [false, true] {
|
||||
let ctx = Context::default();
|
||||
let area = Id::new("cancel-area");
|
||||
let other = Id::new("other-area");
|
||||
let layout = Layout {
|
||||
root: Some(Node::Split {
|
||||
axis: SplitAxis::Horizontal,
|
||||
size: SplitSize::FixedFirst(180.0),
|
||||
first: Box::new(tabs(&[1], 0)),
|
||||
second: Box::new(tabs(&[2], 0)),
|
||||
}),
|
||||
floating: vec![],
|
||||
};
|
||||
let body_id = Id::new("application-body-drag");
|
||||
let mut value = 0.0;
|
||||
let mut run = |events: Vec<Event>| {
|
||||
let mut outputs = Vec::new();
|
||||
ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))), events, ..Default::default() }, |ui| {
|
||||
let style = DockStyle::from_ui(ui);
|
||||
for (id, x) in [(area, 0.0), (other, 400.0)] {
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(Rect::from_min_size(pos2(x, 0.0), vec2(390.0, 580.0))));
|
||||
outputs.push(DockArea::new(id).show(
|
||||
&mut child,
|
||||
&layout,
|
||||
&style,
|
||||
|p| format!("Panel {p}"),
|
||||
|_| Permissions::default(),
|
||||
|ui, p| {
|
||||
if body_drag && id == area && *p == 1 {
|
||||
let rect = Rect::from_min_size(ui.max_rect().min + vec2(10.0, 10.0), vec2(100.0, 40.0));
|
||||
let response = ui.interact(rect, body_id, egui::Sense::drag());
|
||||
if response.dragged() {
|
||||
value += response.drag_delta().x;
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
outputs
|
||||
};
|
||||
for _ in 0..3 {
|
||||
run(vec![]);
|
||||
}
|
||||
let owned = if body_drag { body_id } else { other.with(("split", Vec::<bool>::new())) };
|
||||
let start = ctx.read_response(owned).unwrap().rect.center();
|
||||
run(vec![Event::PointerMoved(start), pointer_button(start, true)]);
|
||||
run(vec![Event::PointerMoved(start + vec2(20.0, 0.0))]);
|
||||
assert_eq!(ctx.dragged_id(), Some(owned));
|
||||
cancel_drag::<u32>(&ctx, area);
|
||||
assert_eq!(ctx.dragged_id(), Some(owned));
|
||||
let outputs = run(vec![Event::PointerMoved(start + vec2(40.0, 0.0))]);
|
||||
assert_eq!(ctx.dragged_id(), Some(owned));
|
||||
if body_drag {
|
||||
assert!(value >= 40.0);
|
||||
} else {
|
||||
assert!(!outputs[1].actions.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user