Fix window dragging with modal dialogs (#4913)
This commit is contained in:
parent
f43d019a72
commit
a0627311ab
|
|
@ -14,6 +14,11 @@ fn rects(ctx: &egui::Context) -> Vec<egui::Rect> {
|
|||
ctx.data(|m| m.get_temp(egui::Id::new(RECTS))).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The window drag target must not cover a dialog which has been moved into the title bar.
|
||||
pub(crate) fn titlebar_drag_area_is_clear(ctx: &egui::Context, area: egui::Rect) -> bool {
|
||||
rects(ctx).iter().all(|dialog| !dialog.intersects(area))
|
||||
}
|
||||
|
||||
/// With a dialog open the rest of the window is inert (egui's modal layer), but like Photoshop the
|
||||
/// image can still be panned and zoomed under it. The pointer position when it is over `canvas`
|
||||
/// and not over a dialog or one of its popups.
|
||||
|
|
|
|||
|
|
@ -1840,6 +1840,9 @@ impl eframe::App for PhotocraftApp {
|
|||
notices::show(self, &ctx);
|
||||
gpu_status::show_fallback(self, &ctx);
|
||||
kys_import::show_offer(self, &ctx);
|
||||
if chrome && self.ui.panels.menu_bar && self.custom_titlebar && !self.ui.dialogs.is_empty() {
|
||||
titlebar::modal_drag_overlay(&ctx);
|
||||
}
|
||||
if self.custom_titlebar {
|
||||
titlebar::resize_zones(ui);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -587,6 +587,7 @@ pub fn title_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
let span_id = ui.id().with("titledrag-span");
|
||||
let (gap_l, gap_r) = ui.ctx().data(|d| d.get_temp::<(f32, f32)>(span_id)).unwrap_or((full.right(), full.right()));
|
||||
let gap = egui::Rect::from_x_y_ranges(gap_l.max(full.left())..=gap_r.min(full.right()).max(gap_l), full.y_range());
|
||||
ui.ctx().data_mut(|d| d.insert_temp(crate::titlebar::modal_drag_area_id(), gap));
|
||||
let drag = ui.interact(gap, ui.id().with("titledrag"), Sense::click_and_drag());
|
||||
if drag.drag_started() {
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ pub const BUTTON_WIDTH: f32 = 46.0;
|
|||
/// (#1581); 8 pt is about a native resize border, and only trims the toolbar buttons' outer 3 pt.
|
||||
const EDGE: f32 = 8.0;
|
||||
const CORNER: f32 = 12.0;
|
||||
const MODAL_DRAG_AREA: &str = "titlebar-modal-drag-area";
|
||||
/// Breeze's Close button on hover and press (KDE's Breeze decoration colours).
|
||||
const BREEZE_CLOSE_HOVER: Color32 = Color32::from_rgb(0xff, 0x98, 0xa2);
|
||||
const BREEZE_CLOSE_PRESSED: Color32 = Color32::from_rgb(0x6d, 0x22, 0x29);
|
||||
|
|
@ -50,6 +51,32 @@ pub enum CaptionFamily {
|
|||
Generic,
|
||||
}
|
||||
|
||||
/// The title bar's unused gap, saved by `panels::title_bar` for drags that begin while a modal
|
||||
/// backdrop is on top of the normal title-bar widgets.
|
||||
pub(crate) fn modal_drag_area_id() -> Id {
|
||||
Id::new(MODAL_DRAG_AREA)
|
||||
}
|
||||
|
||||
/// Restore window dragging over the title bar's empty gap after a modal has blocked the normal
|
||||
/// title-bar layer. The hit target stays limited to the same gap, so menus and caption controls
|
||||
/// remain owned by their widgets.
|
||||
pub(crate) fn modal_drag_overlay(ctx: &egui::Context) {
|
||||
let Some(area) = ctx.data(|d| d.get_temp::<Rect>(modal_drag_area_id())) else { return };
|
||||
if !area.is_positive() || egui::Popup::is_any_open(ctx) || !crate::dialogs::titlebar_drag_area_is_clear(ctx, area) {
|
||||
return;
|
||||
}
|
||||
egui::Area::new(Id::new("titlebar-modal-drag-overlay")).order(Order::Tooltip).fixed_pos(area.min).movable(false).sense(Sense::hover()).show(ctx, |ui| {
|
||||
let response = ui.allocate_rect(area, Sense::click_and_drag());
|
||||
if response.drag_started() {
|
||||
ctx.send_viewport_cmd(ViewportCommand::StartDrag);
|
||||
}
|
||||
if response.double_clicked() {
|
||||
let max = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
|
||||
ctx.send_viewport_cmd(ViewportCommand::Maximized(!max));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl CaptionFamily {
|
||||
/// `windows`, `adwaita`, `breeze` or `generic`, in any case.
|
||||
pub fn parse(name: &str) -> Option<Self> {
|
||||
|
|
@ -530,6 +557,10 @@ mod tests {
|
|||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>) -> egui::FullOutput {
|
||||
self.frame_with_modal(events, false)
|
||||
}
|
||||
|
||||
fn frame_with_modal(&mut self, events: Vec<Event>, modal: bool) -> egui::FullOutput {
|
||||
self.time += 0.05;
|
||||
let mut input =
|
||||
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(self.width, 600.0))), time: Some(self.time), events, ..Default::default() };
|
||||
|
|
@ -541,6 +572,12 @@ mod tests {
|
|||
let app = &mut self.app;
|
||||
let mut out = self.ctx.run_ui(input, |ui| {
|
||||
crate::panels::title_bar(app, ui);
|
||||
if modal {
|
||||
egui::Modal::new(Id::new("titlebar-drag-test-modal")).backdrop_color(Color32::TRANSPARENT).show(ui.ctx(), |ui| {
|
||||
ui.label("Modal dialog");
|
||||
});
|
||||
modal_drag_overlay(ui.ctx());
|
||||
}
|
||||
resize_zones(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
|
|
@ -550,10 +587,17 @@ mod tests {
|
|||
/// Frames for a pointer gesture: move to `path[0]`, press, move along `path`, release. The
|
||||
/// viewport commands the frames sent.
|
||||
fn gesture(&mut self, path: &[Pos2]) -> Vec<ViewportCommand> {
|
||||
self.gesture_with_modal(path, false)
|
||||
}
|
||||
|
||||
fn gesture_with_modal(&mut self, path: &[Pos2], modal: bool) -> Vec<ViewportCommand> {
|
||||
let mut events = vec![vec![Event::PointerMoved(path[0])], vec![button(path[0], true)]];
|
||||
events.extend(path[1..].iter().map(|p| vec![Event::PointerMoved(*p)]));
|
||||
events.push(vec![button(*path.last().unwrap_or(&path[0]), false)]);
|
||||
events.into_iter().flat_map(|e| self.frame(e).viewport_output.remove(&ViewportId::ROOT).map(|v| v.commands).unwrap_or_default()).collect()
|
||||
events
|
||||
.into_iter()
|
||||
.flat_map(|e| self.frame_with_modal(e, modal).viewport_output.remove(&ViewportId::ROOT).map(|v| v.commands).unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn click(&mut self, p: Pos2) -> Vec<ViewportCommand> {
|
||||
|
|
@ -675,6 +719,31 @@ mod tests {
|
|||
assert!(!cmds.iter().any(|c| matches!(c, ViewportCommand::StartDrag)), "{cmds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_bar_space_still_drags_with_a_modal_open() {
|
||||
let mut w = Win::new(1440.0);
|
||||
w.frame(vec![]);
|
||||
w.frame_with_modal(vec![], true);
|
||||
let empty = pos2(980.0, 16.0);
|
||||
let drag_area = w.ctx.data(|d| d.get_temp::<Rect>(modal_drag_area_id()));
|
||||
assert!(drag_area.is_some_and(|r| r.contains(empty)), "the test point must be in the current title-bar gap: {drag_area:?}");
|
||||
assert!(w.bar_widgets().iter().all(|r| !r.contains(empty)), "the probe point must be empty bar space");
|
||||
let cmds = w.gesture_with_modal(&[empty, empty + vec2(12.0, 4.0), empty + vec2(30.0, 8.0)], true);
|
||||
assert_eq!(cmds.iter().filter(|c| matches!(c, ViewportCommand::StartDrag)).count(), 1, "{cmds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modal_drag_overlay_does_not_cover_a_dialog_moved_into_the_gap() {
|
||||
let mut w = Win::new(1440.0);
|
||||
w.frame(vec![]);
|
||||
w.frame_with_modal(vec![], true);
|
||||
let area = w.ctx.data(|d| d.get_temp::<Rect>(modal_drag_area_id())).unwrap_or(Rect::NOTHING);
|
||||
assert!(area.contains(pos2(980.0, 16.0)), "the probe point must be in the gap: {area:?}");
|
||||
w.ctx.data_mut(|d| d.insert_temp(Id::new("pc-dialog-rects"), vec![area]));
|
||||
let cmds = w.gesture_with_modal(&[area.center(), area.center() + vec2(12.0, 4.0)], true);
|
||||
assert!(!cmds.iter().any(|c| matches!(c, ViewportCommand::StartDrag)), "{cmds:?}");
|
||||
}
|
||||
|
||||
/// #1581: KDE Wayland keeps the button release of an OS resize. egui then kept the edge
|
||||
/// pressed (no hover until the next click) and egui-winit never re-sent the resize cursor that
|
||||
/// the compositor had replaced: the cursor came back only on every other press.
|
||||
|
|
|
|||
Loading…
Reference in New Issue