diff --git a/crates/ui/src/docking/chrome.rs b/crates/ui/src/docking/chrome.rs index 824eb44a..86cd8ef2 100644 --- a/crates/ui/src/docking/chrome.rs +++ b/crates/ui/src/docking/chrome.rs @@ -117,6 +117,7 @@ pub struct FloatingFrame<'a> { limits: PanelLimits, resizable: bool, resize_label: &'a str, + opacity: f32, } /// What a [`FloatingFrame`] did this frame. @@ -136,7 +137,7 @@ pub struct FrameResponse { impl<'a> FloatingFrame<'a> { pub fn new(id: Id, rect: Rect) -> Self { - Self { id, rect, bounds: None, limits: PanelLimits::default(), resizable: true, resize_label: "Resize window" } + Self { id, rect, bounds: None, limits: PanelLimits::default(), resizable: true, resize_label: "Resize window", opacity: 1.0 } } /// The frame stays inside `bounds`; the default is the viewport's content rect. @@ -163,6 +164,12 @@ impl<'a> FloatingFrame<'a> { self } + /// Opacity of everything drawn in the frame, contents included (default 1.0). + pub fn opacity(mut self, opacity: f32) -> Self { + self.opacity = opacity; + self + } + pub fn layer(id: Id) -> LayerId { LayerId::new(Order::Middle, id) } @@ -180,6 +187,7 @@ impl<'a> FloatingFrame<'a> { .movable(false) .constrain_to(bounds); let shown = area.show(ctx, |ui| { + ui.multiply_opacity(self.opacity); // Drawn at `rect` itself, also in egui's first (sizing) pass of a new area. ui.set_min_size(rect.size()); ui.allocate_rect(rect, Sense::hover()); diff --git a/crates/ui/src/docking/floating_tests.rs b/crates/ui/src/docking/floating_tests.rs index 78365b3e..3a76bce3 100644 --- a/crates/ui/src/docking/floating_tests.rs +++ b/crates/ui/src/docking/floating_tests.rs @@ -51,6 +51,8 @@ struct Content { /// Canvas 0 accepts side splits, as an app that opens panels beside it does. beside: bool, drawn: Vec, + /// A preview the host draws for every drag, from `resolve_drag`. + host_preview: Option, } impl DockContent for Content { @@ -71,6 +73,9 @@ impl DockContent for Content { fn allow_native(&self, _: &u32) -> bool { self.native } + fn resolve_drag(&mut self, _: &Context, _: &DockStyle, _: &DropRequest<'_, u32>) -> Option { + self.host_preview.map(|rect| DropFeedback { preview: Some(rect), insertion: None }) + } } #[derive(Clone, Copy, PartialEq)] @@ -106,6 +111,8 @@ struct Driver { hide: Hide, modifiers: Modifiers, time: f64, + /// What the last frame painted. + shapes: Vec, } impl Driver { @@ -119,6 +126,7 @@ impl Driver { hide: Hide::None, modifiers: Modifiers::NONE, time: 0.0, + shapes: Vec::new(), }; driver.step(vec![]); driver.step(vec![]); @@ -154,6 +162,7 @@ impl Driver { }); let mut full = full; full.textures_delta.clear(); + self.shapes = full.shapes; output } @@ -194,6 +203,22 @@ impl Driver { self.step(vec![button(at, false)]) } + /// Opacity of the floating window that the last frame drew at `rect`: the alpha of its + /// opaque background fill. + fn window_opacity(&self, rect: [f32; 4]) -> Option { + let rect = Rect::from_min_size(pos2(rect[0], rect[1]), vec2(rect[2], rect[3])); + fn find(shape: &egui::Shape, rect: Rect) -> Option { + match shape { + egui::Shape::Vec(shapes) => shapes.iter().find_map(|s| find(s, rect)), + egui::Shape::Rect(r) if r.rect.min.distance(rect.min) < 0.5 && r.rect.max.distance(rect.max) < 0.5 && r.fill.a() > 0 => { + Some(f32::from(r.fill.a()) / 255.0) + } + _ => None, + } + } + self.shapes.iter().find_map(|s| find(&s.shape, rect)) + } + fn wait(&mut self) { for _ in 0..45 { self.step(vec![]); @@ -728,3 +753,59 @@ fn undock_panel_group_floats_one_group_of_a_floating_window() { menu(&mut h, 5); assert_eq!(h.query_all_by_label("Undock Panel Group").count(), 0); } + +#[test] +fn a_title_bar_drag_fades_over_a_preview_the_host_draws() { + let mut d = Driver::new(workspace(), false); + d.content.host_preview = Some(Rect::from_min_size(pos2(20.0, 20.0), vec2(100.0, 100.0))); + let title = d.title(8).center(); + d.step(vec![Event::PointerMoved(title), button(title, true)]); + d.step(vec![Event::PointerMoved(title + vec2(8.0, 6.0))]); + d.step(vec![Event::PointerMoved(pos2(650.0, 150.0))]); + let rect = window_of(&d.layout, 8).unwrap().rect; + d.step(vec![]); + let shown = d.window_opacity(rect).unwrap(); + assert!((shown - 0.75).abs() < 0.01, "{shown}"); +} + +#[test] +fn an_in_window_floating_window_fades_only_while_its_title_bar_drag_highlights_a_drop_target() { + let mut d = Driver::new(workspace(), false); + let strip = d.tab(4).unwrap().center(); + let empty = pos2(650.0, 150.0); + // Opacity of window W2 (dragged) and W1 once the pointer rests at `at`: the window draws + // before the preview, so it fades a frame after the target lights up. + let over = |d: &mut Driver, at: egui::Pos2| { + d.step(vec![Event::PointerMoved(at)]); + let (dragged, other) = (window_of(&d.layout, 8).unwrap().rect, window_of(&d.layout, 5).unwrap().rect); + d.step(vec![]); + (d.window_opacity(dragged).unwrap(), d.window_opacity(other).unwrap()) + }; + let shown = |d: &Driver| d.window_opacity(window_of(&d.layout, 8).unwrap().rect).unwrap(); + let grab = |d: &mut Driver| { + let title = d.title(8).center(); + d.step(vec![Event::PointerMoved(title), button(title, true)]); + d.step(vec![Event::PointerMoved(title + vec2(8.0, 6.0))]); + }; + assert_eq!(shown(&d), 1.0); + grab(&mut d); + assert_eq!(over(&mut d, empty), (1.0, 1.0), "over empty space"); + let (dragged, other) = over(&mut d, strip); + assert!((dragged - 0.75).abs() < 0.01 && other == 1.0, "over a drop target: {dragged}, {other}"); + assert_eq!(over(&mut d, empty).0, 1.0, "after leaving the target"); + assert!((over(&mut d, strip).0 - 0.75).abs() < 0.01); + // Released off the target the window stays floating, opaque. + d.step(vec![Event::PointerMoved(empty), button(empty, false)]); + d.step(vec![]); + assert_eq!(shown(&d), 1.0, "after the release"); + assert!(window_of(&d.layout, 8).is_some()); + + // Escape over a target puts the window back, opaque. + grab(&mut d); + assert!((over(&mut d, strip).0 - 0.75).abs() < 0.01); + d.step(vec![Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }]); + d.step(vec![]); + assert_eq!(shown(&d), 1.0, "after Escape"); + d.step(vec![button(strip, false)]); + assert!(window_of(&d.layout, 8).is_some()); +} diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 7f473881..19e7697f 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -2360,15 +2360,41 @@ fn a_tab_drag_onto_a_native_window_is_no_app_drag() { } #[test] -fn a_native_window_fades_only_while_its_drag_can_dock() { - let drag = |docks| WindowDrag { window: 1, grab: vec2(10.0, 10.0), to: pos2(100.0, 100.0), start: pos2(100.0, 100.0), docks }; - assert_eq!(drag_opacity(None, false, true), 1.0); - // A bar drag that only moves the window stays opaque. - assert_eq!(drag_opacity(Some(&drag(false)), false, false), 1.0); - assert_eq!(drag_opacity(Some(&drag(false)), false, true), 0.75); - // A drag by the window's only tab or group docks either way. - assert_eq!(drag_opacity(Some(&drag(true)), false, false), 0.75); - // With Cmd/Ctrl held the drag only moves the window. - assert_eq!(drag_opacity(Some(&drag(false)), true, true), 1.0); - assert_eq!(drag_opacity(Some(&drag(true)), true, false), 1.0); +fn a_native_window_fades_only_while_its_bar_drag_highlights_a_drop_target() { + let bar_to = |d: &mut DockDriver, to: Pos2| { + let id = window(d, 5).id; + let local = d.window_local(id, ORIGIN + to.to_vec2()).unwrap(); + d.window_step(id, vec![Event::PointerMoved(local)]); + drag_opacity(&d.ctx, d.id) + }; + let grab = |d: &mut DockDriver| { + let id = window(d, 5).id; + d.window_step(id, vec![Event::PointerMoved(pos2(40.0, 9.0)), button(pos2(40.0, 9.0), true)]); + d.window_step(id, vec![Event::PointerMoved(pos2(60.0, 19.0))]); + id + }; + // A bar that only moves its window highlights nothing. + let mut d = driver(layout(true)).with_style(|s| { + s.command_press_floats = true; + s.title_bar_docks = false; + }); + grab(&mut d); + assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 1.0, "a move that cannot dock"); + + let mut d = driver(layout(true)); + let id = grab(&mut d); + // Past the main window's right side: the desktop. + assert_eq!(bar_to(&mut d, pos2(1000.0, 300.0)), 1.0, "over empty space"); + // The right edge zone of frame [3, 4]. + assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75, "over a drop target"); + // The middle of the frame is no target for a whole window. + assert_eq!(bar_to(&mut d, pos2(600.0, 300.0)), 1.0, "after leaving the target"); + assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75); + // With Cmd/Ctrl held the drop floats and nothing is highlighted. + d.modifiers = Modifiers::COMMAND; + assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 1.0, "a floating drop"); + d.modifiers = Modifiers::NONE; + assert_eq!(bar_to(&mut d, pos2(760.0, 300.0)), 0.75); + d.window_step(id, vec![button(pos2(40.0, 9.0), false)]); + assert_eq!(drag_opacity(&d.ctx, d.id), 1.0, "after the drop"); } diff --git a/crates/ui/src/docking/render/cross.rs b/crates/ui/src/docking/render/cross.rs index 5376788e..a868744f 100644 --- a/crates/ui/src/docking/render/cross.rs +++ b/crates/ui/src/docking/render/cross.rs @@ -25,6 +25,9 @@ pub(super) const POINTER: &str = "drag-pointer"; /// Key of an Escape that reached one of the application's windows other than the drag's /// owner. Keys go to the window with keyboard focus, which need not be the one dragging. const ESCAPE: &str = "drag-escape"; +/// Key of whether a native window's bar drag highlighted a drop target on the last frame, under +/// the area's id: [`Cross::highlighted`] as [`Renderer::end_cross`] left it. +pub(super) const HIGHLIGHTED: &str = "drag-highlighted"; /// The pointer of a drag, as the window that owns the drag last saw it. #[derive(Clone, Copy, Debug, PartialEq)] @@ -99,6 +102,9 @@ pub(in crate::docking) struct Cross

{ pub moved: Option, /// Resolving a window move that docks whatever `title_bar_docks` says. pub docks: bool, + /// A native window's bar drag highlighted a drop target this frame, in whichever window + /// drew its preview. + pub highlighted: bool, /// A native window's dragged widget is one of the area's controls: egui keeps a drag per /// viewport. pub held: bool, @@ -122,6 +128,7 @@ impl

Default for Cross

{ bar: false, moved: None, docks: false, + highlighted: false, held: false, pointed: None, viewports: Vec::new(), @@ -315,7 +322,7 @@ impl Renderer<'_, P> { let Some(drag) = pointer.window.and_then(|w| self.window_group_drag(w)) else { return }; if let Some(Dest::Main(local)) = self.destination(&pointer, None) { (self.cross.bar, self.cross.docks) = (true, pointer.docks); - self.paint_group_drag(ui, &drag, local, pointer.command); + self.cross.highlighted |= self.paint_group_drag(ui, &drag, local, pointer.command); (self.cross.bar, self.cross.docks) = (false, false); } } @@ -371,7 +378,7 @@ impl Renderer<'_, P> { if let Some(window) = pointer.window { if let Some(drag) = self.window_group_drag(window) { self.cross.docks = pointer.docks; - self.paint_group_drag(ui, &drag, local, pointer.command); + self.cross.highlighted |= self.paint_group_drag(ui, &drag, local, pointer.command); self.cross.docks = false; } return; @@ -391,9 +398,12 @@ impl Renderer<'_, P> { } } - /// End of the main window's frame: forget the pointer of a finished drag. + /// End of the main window's frame: forget the pointer of a finished drag, and keep whether + /// a bar drag highlighted a drop target. The dragged window sets its opacity from that on + /// the next frame: it draws before the main window paints its preview. pub(in crate::docking) fn end_cross(&mut self, ctx: &Context) { let id = self.id; + ctx.data_mut(|d| d.insert_temp(id.with(HIGHLIGHTED), self.cross.highlighted)); let active = ctx.data(|d| { d.get_temp::>(id.with("drag")).is_some() || d.get_temp::>(id.with("group-drag")).is_some() diff --git a/crates/ui/src/docking/render/floating.rs b/crates/ui/src/docking/render/floating.rs index ceab8df8..256b128f 100644 --- a/crates/ui/src/docking/render/floating.rs +++ b/crates/ui/src/docking/render/floating.rs @@ -80,11 +80,13 @@ impl Renderer<'_, P> { let movable = window.panels().all(|panel| self.permissions(panel).movable); let style = ChromeStyle::from_dock(self.style); let resize_label = self.style.resize_window_label.clone(); - let response = FloatingFrame::new(id, rect).bounds(screen).limits(limits).resizable(movable).resize_label(&resize_label).show( - ctx, - &style, - |ui, contents| self.window_contents(ui, window, contents, rect, &style), - ); + let opacity = self.drag_opacity(ctx, first); + let response = + FloatingFrame::new(id, rect).bounds(screen).limits(limits).resizable(movable).resize_label(&resize_label).opacity(opacity).show( + ctx, + &style, + |ui, contents| self.window_contents(ui, window, contents, rect, &style), + ); for handle in &response.handles { self.register_interaction(ctx, handle.id); } @@ -104,6 +106,18 @@ impl Renderer<'_, P> { } } + /// Opacity of the in-window floating window whose first panel is `first`: faded + /// ([`super::DRAG_OPACITY`]) while its title bar drag highlights a drop target, as the last + /// frame drew it ([`super::preview::GROUP_HIGHLIGHTED`]), else opaque. Its tabs and drag + /// bars only drag a ghost, so the window stays opaque for them. + fn drag_opacity(&self, ctx: &Context, first: &P) -> f32 { + let moving = ctx + .data(|d| d.get_temp::>(self.id.with("group-drag"))) + .is_some_and(|drag| drag.gesture == GroupGesture::Window && drag.panel == *first); + let highlighted = ctx.data(|d| d.get_temp::(self.id.with(super::preview::GROUP_HIGHLIGHTED))).unwrap_or(false); + if moving && highlighted { super::DRAG_OPACITY } else { 1.0 } + } + /// The title bar, then the window's tree below it, in `contents`. Moves carry the whole /// `frame`, resize band included. fn window_contents(&mut self, ui: &mut Ui, window: &Floating

, contents: Rect, frame: Rect, style: &ChromeStyle) { diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index a67f2274..20ce2130 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -37,6 +37,10 @@ pub(in crate::docking) use native::{DesktopCursor, InputHook, set_input_hook}; #[cfg(feature = "native-windows")] const BLOCKED_FOCUS_WAIT: f64 = 0.2; +/// Opacity of a floating window, in-window or native, while its bar, only tab or only tab row +/// drags it over a drop target: the highlighted target under the window shows through it. +pub(in crate::docking) const DRAG_OPACITY: f32 = 0.75; + /// While input is blocked (`starts`: from this frame), a native window of the dock area `area` /// with the keyboard would drop the keys the modal needs, so once, after /// [`BLOCKED_FOCUS_WAIT`], the main window, where the modal is, takes the keyboard if one still diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index 8b477797..d9aab434 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -95,21 +95,16 @@ pub(in crate::docking) const MACOS_CLOSE: CloseLayout = pub(in crate::docking) const OTHER_CLOSE: CloseLayout = CloseLayout { glyph_top: 4.5, glyph_trailing: 11.5, glyph_size: 9.0, hit_left: 4.5, hit_right: 4.5, hit_up: 4.5, hit_down: 4.5 }; -/// Opacity of a native window while its bar or only tab drags it somewhere it can dock: the -/// window stays above the main window, so the drop preview under the pointer shows only -/// through it. +/// Opacity of the native window that a bar drag of the area `area` moves, by the rule for every +/// floating window ([`super::DRAG_OPACITY`]): faded while the drag highlights a drop target, as +/// the last frame drew it ([`super::cross::HIGHLIGHTED`]), opaque over empty space, for a move +/// that cannot dock and while Ctrl/Cmd keeps the drop floating. The window stays above the main +/// window, so the highlighted target shows only through it. #[cfg(any(target_os = "macos", test))] -const DRAG_OPACITY: f64 = 0.75; - -/// Opacity of the window `drag` moves: faded while the drag can dock (its only tab or group, or -/// a bar with [`crate::docking::DockStyle::title_bar_docks`]), opaque for a plain move and while -/// Ctrl/Cmd keeps the drop floating (`float_only`). -#[cfg(any(target_os = "macos", test))] -pub(in crate::docking) fn drag_opacity(drag: Option<&WindowDrag>, float_only: bool, title_bar_docks: bool) -> f64 { - match drag { - Some(drag) if !float_only && (drag.docks || title_bar_docks) => DRAG_OPACITY, - _ => 1.0, - } +pub(in crate::docking) fn drag_opacity(ctx: &Context, area: Id) -> f64 { + let dragging = ctx.data(|d| d.get_temp::(area.with("window-drag"))).is_some(); + let highlighted = ctx.data(|d| d.get_temp::(area.with(super::cross::HIGHLIGHTED))).unwrap_or(false); + if dragging && highlighted { f64::from(super::DRAG_OPACITY) } else { 1.0 } } /// Sets the opacity of the AppKit windows titled `title`, never one titled `main` (the main @@ -645,17 +640,17 @@ impl Renderer<'_, P> { (place.known[0], place.known[1], place.pending) = (to.x, to.y, Some(now + MOVE_WAIT)); } let moving = moving(ctx); - // Set every frame from the drag state, so any way a drag ends (a drop, Escape, a focus - // loss, the window closing) brings the window back to full opacity. AppKit finds the - // window by title, so a window sharing the dragged one's title stays faded with it. + // Set every frame from the drag state and the last frame's preview, so leaving a target + // or any way a drag ends (a drop, Escape, a focus loss, the window closing) brings the + // window back to full opacity. AppKit finds the window by title, so a window sharing + // the dragged one's title stays faded with it. #[cfg(target_os = "macos")] { let main = ctx.input(|i| i.raw.viewports.get(&ViewportId::ROOT).and_then(|v| v.title.clone())); let drag = ctx.data(|d| d.get_temp::(drag_key)); let dragged = drag.as_ref().and_then(|drag| self.layout.floating.iter().find(|w| w.id == drag.window)); let this = dragged.and_then(|w| crate::docking::dock_model::first_active(&w.root)).is_some_and(|p| self.label_of(p) == title); - let float_only = drag.is_some_and(|drag| self.cross.pointer.is_some_and(|p| p.window == Some(drag.window) && p.command)); - let opacity = if this { drag_opacity(drag.as_ref(), float_only, self.style.title_bar_docks) } else { 1.0 }; + let opacity = if this { drag_opacity(ctx, self.id) } else { 1.0 }; set_window_opacity(&title, main.as_deref(), opacity); } // The collapsed height a minimized window reports is not the size to keep. diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs index 0985d2b7..65c15fd3 100644 --- a/crates/ui/src/docking/render/preview.rs +++ b/crates/ui/src/docking/render/preview.rs @@ -33,6 +33,11 @@ const DOCKED_FLOAT_HEIGHT: f32 = 320.0; /// Least height of a floating window or flyout fitted to a short panel's content. pub(super) const MIN_FITTED_HEIGHT: f32 = 120.0; +/// Key of whether the area's group drag highlighted a drop target on the last frame, under the +/// area's id. An in-window floating window that its bar drag moves fades from it on the next +/// frame: the window draws before the preview. +pub(super) const GROUP_HIGHLIGHTED: &str = "group-drag-highlighted"; + /// What finishing a drag reads of the pointer, in the coordinates of the window finishing it. #[derive(Clone, Copy, Debug)] pub(super) struct DragInput { @@ -71,6 +76,7 @@ impl DragInput { impl Renderer<'_, P> { pub(in crate::docking) fn finish_group_drag(&mut self, ui: &Ui) { let ctx = ui.ctx(); + ctx.data_mut(|d| d.remove::(self.id.with(GROUP_HIGHLIGHTED))); let key = self.id.with("group-drag"); let Some(drag) = ctx.data(|d| d.get_temp::>(key)) else { return }; if !drag.members.iter().all(|p| self.layout.contains(p) && self.permissions(p).movable) { @@ -122,7 +128,9 @@ impl Renderer<'_, P> { } } else if let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) { self.output.group_drag = Some(drag.panel.clone()); - self.paint_group_drag(ui, &drag, pointer, float_only); + if self.paint_group_drag(ui, &drag, pointer, float_only) { + ctx.data_mut(|d| d.insert_temp(self.id.with(GROUP_HIGHLIGHTED), true)); + } } } @@ -396,7 +404,8 @@ impl Renderer<'_, P> { /// The ghost at the pointer (a window moving live is its own ghost) under one destination /// preview. The ghost has a translucent fill, a 2 pt outline and the members' labels. - pub(super) fn paint_group_drag(&mut self, ui: &Ui, drag: &GroupDrag

, pointer: Pos2, float_only: bool) { + /// Returns whether it highlighted a drop target. + pub(super) fn paint_group_drag(&mut self, ui: &Ui, drag: &GroupDrag

, pointer: Pos2, float_only: bool) -> bool { let ctx = ui.ctx(); let painter = self.preview_painter(ctx); if drag.gesture != GroupGesture::Window { @@ -418,16 +427,17 @@ impl Renderer<'_, P> { } } if float_only || (drag.gesture == GroupGesture::Window && !self.title_bar_docks()) { - return; + return false; } if let Some((action, drop)) = self.dock_group_proposal(drag, pointer) { + let highlighted = action.is_some(); if let Some(action) = action { self.paint_dock_preview(ctx, &painter, action, &drag.members, &drop); if drag.gesture != GroupGesture::Window { self.paint_float_hint(ctx, pointer); } } - return; + return highlighted; } let proposal = self.group_proposal(drag, pointer).filter(|p| !p.noop); let zones = self.paint_group_zones(&painter, drag, pointer, proposal.as_ref()); @@ -441,7 +451,9 @@ impl Renderer<'_, P> { if drag.gesture != GroupGesture::Window { self.paint_float_hint(ctx, pointer); } + return true; } + false } pub(in crate::docking) fn finish_drag(&mut self, ctx: &Context) { @@ -875,6 +887,8 @@ impl Renderer<'_, P> { let painter = self.preview_painter(ctx); if let Some(rect) = feedback.preview.filter(|rect| crate::docking::targets::usable(*rect)) { drop_preview(&painter, rect, self.style.accent, true); + // A window moving by its title bar fades over it, as over the renderer's own. + ctx.data_mut(|d| d.insert_temp(self.id.with(GROUP_HIGHLIGHTED), true)); } if let Some(rect) = feedback.insertion.filter(|rect| crate::docking::targets::usable(*rect)) { painter.rect_filled(rect, 0.0, self.style.accent); diff --git a/docs/ui-docking.md b/docs/ui-docking.md index aff09198..7a08ad25 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -215,7 +215,9 @@ A flyout fits its content the same way, without the title bar. - A tab drags one panel. A group's drag bar (the space beside its tabs) or its top border drags the whole group. A column bar drags a column. A floating window's title bar drags the window; with `title_bar_docks` a release over a target docks the whole window there, else it only - moves. + moves. While the drag highlights a drop target, the window is drawn at 75% opacity so the + target shows through it; over empty space, and once the drag ends, it is opaque. A native + window moved by its only tab or tab row fades the same way. - Inside a floating window, tabs and drag bars drag and dock as they do in the docked layout, into any window. A drag bar or border of a window's only group takes the whole window along, so, like the title bar, it looks through the window: the groups, docks and windows behind it