diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index dd85edd4..b5fc3bbd 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -18,6 +18,8 @@ serde = { version = "1", features = ["derive"], optional = true } serde = ["dep:serde"] # Floating windows may move to their own OS windows (desktop egui backends with viewports). native-windows = [] +# `docking::testing`: a headless driver for application docking tests. +testing = [] [dev-dependencies] egui = { workspace = true, features = ["default_fonts"] } diff --git a/crates/ui/src/docking/hits.rs b/crates/ui/src/docking/hits.rs new file mode 100644 index 00000000..26c25d91 --- /dev/null +++ b/crates/ui/src/docking/hits.rs @@ -0,0 +1,124 @@ +//! Where each docking gesture starts, for control protocols and tests. + +use std::{collections::HashMap, fmt::Debug, hash::Hash}; + +use egui::{Context, Id, Rect}; + +use super::{DockArea, Layout, Node, render::RESIZE_HANDLES}; + +/// A rendered docking control that starts a gesture. Groups, columns and windows are named by +/// their first panel. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum Hit
{ + /// A tab: click activates, drag moves the panel (S1). + Tab(P), + /// A group's drag bar: click opens the group menu, drag moves the group (S2). + DragBar(P), + /// The top border of a group's strip: drag moves the group (S3). + Border(P), + /// The "…" button of a group whose tabs overflow; named by the active panel. + Overflow(P), + /// [D] A column bar: drag moves the column (S6). + ColumnBar(P), + /// [D] The »/« button of a column bar (M2). + Collapse(P), + /// An accordion entry's −/+ button. + StackToggle(P), + /// A floating window's title bar (S5). + Title(P), + /// A floating window's × button. + Close(P), + /// [D] A panel's icon in an iconic column (S7, M5). + Icon(P), + /// [D] The grip above an icon group (S8). + IconGrip(P), + /// A floating window's resize handle. + Resize(P, ResizeEdge), + Divider(Divider
), +} + +/// The edge or corner a floating window's resize handle moves. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ResizeEdge { + Left, + Right, + Bottom, + BottomLeft, + BottomRight, +} + +/// A divider that resizes what is on either side of it. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum Divider
{
+ /// A split, by its path from the root (see [`super::Action::ResizeSplit`]).
+ Split(Vec , Rect)> {
+ ctx.data(|d| d.get_temp:: , owned: &[Id]) {
+ let mut names: HashMap , Rect)> =
+ owned.iter().filter_map(|id| Some((names.remove(id)?, ctx.read_response(*id)?.rect))).filter(|(_, rect)| rect.is_positive()).collect();
+ ctx.data_mut(|d| d.insert_temp(key(area), hits));
+}
diff --git a/crates/ui/src/docking/iconic_tests.rs b/crates/ui/src/docking/iconic_tests.rs
new file mode 100644
index 00000000..67e722d2
--- /dev/null
+++ b/crates/ui/src/docking/iconic_tests.rs
@@ -0,0 +1,324 @@
+//! [D] Iconic columns, flyouts and hit rects, driven through `testing::DockDriver`.
+
+use super::testing::DockDriver;
+use super::*;
+use crate::layout::{SplitAxis, SplitSize};
+use egui::{Key, Pos2, Rect, pos2, vec2};
+
+struct Content;
+
+impl DockContent (column: &Column ) -> f32 {
- if column.iconic { column.iconic_width.unwrap_or(ICONIC_WIDTH) } else { column.width }
+pub(in crate::docking) fn column_width (column: &Column , style: &DockStyle) -> f32 {
+ if column.iconic {
+ column.iconic_width.unwrap_or(style.iconic_width).clamp(style.iconic_width, style.iconic_max_width)
+ } else {
+ column.width
+ }
}
/// Heights of stacked groups in a column body `room` high. Each entry is (minimized, saved
@@ -104,7 +104,7 @@ impl (P);
+
+fn key(area: Id) -> Id {
+ area.with("flyout")
+}
+
+/// [D] Open the flyout of the group holding `panel`, when that group is in an iconic column.
+/// The flyout shows from the next frame on. [`crate::docking::DockArea::raise_panel`] does the
+/// same for a panel the Window menu opens.
+pub fn open_flyout {
+ ctx.data(|d| d.get_temp:: {
+ group: Group ,
+ /// The icons of the group, where the flyout's top aligns.
+ icons: Rect,
+ /// The iconic column, which a press does not close the flyout in.
+ column: Rect,
+ edge: Edge,
+ /// The column's expanded width.
+ width: f32,
+}
+
+/// Per-frame iconic state of a renderer.
+pub(in crate::docking) struct Iconic {
+ auto_collapse: bool,
+ /// Iconic column bodies drawn this frame, outlined by a drop into them (P9).
+ columns: Vec {
+ pub(in crate::docking) fn new(auto_collapse: bool) -> Self {
+ Self { auto_collapse, columns: Vec::new(), widths: Vec::new(), request: None, rect: None }
+ }
+
+ pub(super) fn covers(&self, pointer: Pos2) -> bool {
+ self.rect.is_some_and(|rect| rect.contains(pointer))
+ }
+
+ /// Outline the iconic column an insertion line lies in.
+ pub(super) fn outline(&self, painter: &egui::Painter, line: Rect, style: &DockStyle) {
+ if let Some(column) = self.columns.iter().find(|column| column.shrink(1.0).contains(line.center())) {
+ painter.rect_stroke(column.shrink(1.0), 0.0, Stroke::new(2.0, style.accent), StrokeKind::Inside);
+ }
+ }
+
+ pub(super) fn float_width(&self, panel: &P) -> Option ) -> f32 {
+ GRIP + group.panels.len() as f32 * self.row_height()
+ }
+
+ /// The body of an iconic column: icon groups top to bottom, scrolling when they do not fit.
+ pub(super) fn iconic_column(&mut self, ui: &mut Ui, column: &Column , body: Rect, edge: Edge) {
+ self.iconic.columns.push(body);
+ for panel in column.groups.iter().flat_map(|g| g.panels.iter()) {
+ self.iconic.widths.push((panel.clone(), column.width));
+ }
+ // Opening a panel from the Window menu opens its flyout (U3).
+ if let Some(panel) = self.activation.as_ref().filter(|p| column.groups.iter().any(|g| g.panels.contains(p))) {
+ open_flyout(ui.ctx(), self.id, panel.clone());
+ }
+ let total: f32 = column.groups.iter().map(|g| self.icon_group_height(g)).sum:: , area: Rect, view: Rect, edge: Edge) {
+ let open = flyout:: (ui.ctx(), self.id);
+ let labels = view.width() >= self.style.iconic_label_width;
+ let xs = view.x_range();
+ let mut top = area.top();
+ let mut bands = Vec::new();
+ for (index, group) in column.groups.iter().enumerate() {
+ let Some(first) = group.panels.first() else { continue };
+ if index > 0 {
+ let y = top + SEPARATOR * 0.5;
+ ui.painter().with_clip_rect(view).hline(area.x_range().shrink(4.0), y, self.style.border);
+ top += SEPARATOR;
+ }
+ let rect = Rect::from_min_size(pos2(area.left(), top), vec2(area.width(), self.icon_group_height(group)));
+ let grip = Rect::from_min_size(rect.min, vec2(rect.width(), GRIP));
+ self.icon_grip(ui, group, grip, column.width);
+ let showing = open.as_ref().filter(|p| group.panels.contains(p));
+ let mut y = grip.bottom();
+ for panel in &group.panels {
+ let cell = Rect::from_min_size(pos2(rect.left(), y), vec2(rect.width(), self.row_height()));
+ self.icon(ui, panel, cell, labels, showing);
+ y = cell.bottom();
+ }
+ // The state again: a click above may have opened or closed this group's flyout.
+ if flyout:: (ui.ctx(), self.id).is_some_and(|p| group.panels.contains(&p)) {
+ self.iconic.request = Some(Request { group: group.clone(), icons: rect.intersect(view), column: view, edge, width: column.width });
+ }
+ bands.push((first.clone(), rect));
+ top = rect.bottom();
+ }
+ // P9: the upper half of an icon group inserts above it, the lower half and the room
+ // under the last group below it. The line sits on the group's edge.
+ let count = bands.len();
+ for (index, (anchor, rect)) in bands.into_iter().enumerate() {
+ let middle = rect.center().y;
+ let upper_top = if index == 0 { view.top() } else { rect.top() - SEPARATOR * 0.5 };
+ let lower_bottom = if index + 1 == count { view.bottom() } else { rect.bottom() + SEPARATOR * 0.5 };
+ for (below, ys, line) in [(false, upper_top..=middle, rect.top()), (true, middle..=lower_bottom, rect.bottom())] {
+ let band = Rect::from_x_y_ranges(xs, ys).intersect(view);
+ if usable(band) {
+ let line = horizontal_line(line.clamp(view.top() + 2.0, view.bottom() - 2.0), xs);
+ self.dock_targets.push(DockDrop { target: DockTarget::GroupBand { anchor: anchor.clone(), below }, band, line });
+ }
+ }
+ }
+ }
+
+ /// The grip above an icon group drags the whole group (S8).
+ fn icon_grip(&mut self, ui: &mut Ui, group: &Group , grip: Rect, width: f32) {
+ let Some(first) = group.panels.first() else { return };
+ let movable = group.panels.iter().all(|p| self.permissions(p).movable);
+ let response = ui.interact(grip, self.id.with(("icon-grip", Some(first))), Sense::drag());
+ let response = if movable { response.on_hover_cursor(egui::CursorIcon::Grab) } else { response };
+ self.register_interaction(ui.ctx(), response.id);
+ response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.move_group_label));
+ let color = if response.hovered() || response.dragged() { self.style.text } else { self.style.inactive_text };
+ let painter = ui.painter().with_clip_rect(grip.intersect(ui.clip_rect()));
+ painter.hline((grip.center().x - 8.0)..=(grip.center().x + 8.0), grip.center().y - 1.0, Stroke::new(1.0, color));
+ painter.hline((grip.center().x - 8.0)..=(grip.center().x + 8.0), grip.center().y + 1.0, Stroke::new(1.0, color));
+ if movable && response.drag_started() {
+ let origin = ui.input(|i| i.pointer.press_origin()).or_else(|| ui.ctx().pointer_interact_pos()).unwrap_or(grip.center());
+ let height = group.height.filter(|h| h.is_finite()).unwrap_or(FLYOUT_HEIGHT);
+ let source = Rect::from_min_size(grip.min, vec2(width.max(self.style.min_pane), height.max(self.style.tab_height)));
+ let drag = GroupDrag {
+ panel: first.clone(),
+ original: [source.left(), source.top(), source.width(), source.height()],
+ gesture: GroupGesture::Group,
+ members: group.panels.clone(),
+ origin,
+ source,
+ };
+ ui.ctx().data_mut(|d| d.insert_temp(self.id.with("group-drag"), drag));
+ close_flyout:: (ui.ctx(), self.id);
+ }
+ }
+
+ /// One panel's icon: a click opens, switches or closes the flyout (M5); a drag moves the
+ /// panel (S7). `showing` is the panel whose icon opened this group's flyout.
+ fn icon(&mut self, ui: &mut Ui, panel: &P, cell: Rect, labels: bool, showing: Option<&P>) {
+ let movable = self.permissions(panel).movable;
+ let label = self.label_of(panel);
+ let response = ui.interact(cell, self.id.with(("icon", panel)), if movable { Sense::click_and_drag() } else { Sense::click() });
+ self.register_interaction(ui.ctx(), response.id);
+ let current = showing == Some(panel);
+ response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), current, &label));
+ let painter = ui.painter().with_clip_rect(cell.intersect(ui.clip_rect()));
+ if current || response.hovered() {
+ painter.rect_filled(cell.shrink(2.0), 3.0, self.style.active_background);
+ }
+ if response.has_focus() {
+ painter.rect_stroke(cell.shrink(1.0), 3.0, Stroke::new(1.0, self.style.accent), StrokeKind::Inside);
+ }
+ let size = self.style.icon_size;
+ let inset = ((self.style.iconic_width - size) * 0.5).max(2.0);
+ let icon = Rect::from_min_size(pos2(cell.left() + inset, cell.center().y - size * 0.5), vec2(size, size));
+ let color = if current { self.style.text } else { self.style.inactive_text };
+ if let Some(source) = self.content.icon(panel) {
+ egui::Image::new(source).tint(color).paint_at(ui, icon.intersect(cell));
+ } else {
+ let initial: String = label.chars().next().into_iter().flat_map(char::to_uppercase).collect();
+ painter.rect_stroke(icon, 3.0, Stroke::new(1.0, color), StrokeKind::Inside);
+ painter.text(icon.center(), egui::Align2::CENTER_CENTER, initial, self.style.font.clone(), color);
+ }
+ if labels {
+ let text = self.content.label(ui, panel, &label, &self.style.font, color);
+ let galley = text.into_galley(ui, Some(egui::TextWrapMode::Truncate), (cell.right() - icon.right() - 12.0).max(0.0), self.style.font.clone());
+ painter.galley(pos2(icon.right() + 6.0, cell.center().y - galley.size().y * 0.5), galley, color);
+ } else if response.hovered() && !response.dragged() {
+ response.clone().on_hover_text(&label);
+ }
+ if response.clicked() {
+ if current {
+ close_flyout:: (ui.ctx(), self.id);
+ } else {
+ if showing.is_none() {
+ ui.ctx().move_to_top(FloatingFrame::layer(self.id.with("flyout")));
+ }
+ open_flyout(ui.ctx(), self.id, panel.clone());
+ self.output.actions.push(crate::docking::Action::Activate { panel: panel.clone() });
+ self.output.focused = Some(panel.clone());
+ }
+ }
+ if movable && response.drag_started() && begin_drag(ui.ctx(), self.id, panel.clone(), cell) {
+ close_flyout:: (ui.ctx(), self.id);
+ }
+ }
+
+ /// The Auto-Collapse Iconic Panels item of a column menu (U1): true when chosen.
+ pub(super) fn auto_collapse_item(&mut self, ui: &mut Ui) -> bool {
+ let logical = self.style.auto_collapse_label.clone();
+ let text = self.content.action_label(ui, &logical, &self.style.font, self.style.text);
+ let mut on = self.iconic.auto_collapse;
+ let response = ui.add(egui::Checkbox::new(&mut on, text));
+ response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), self.iconic.auto_collapse, &logical));
+ if response.clicked() {
+ ui.close();
+ }
+ response.clicked()
+ }
+
+ /// Draw the flyout an iconic column asked for, beside the column on the canvas side, or
+ /// forget a flyout whose group is no longer iconic. With Auto-Collapse, Escape or a press
+ /// outside the flyout and its column closes it.
+ pub(in crate::docking) fn flyout(&mut self, ctx: &Context) {
+ let Some(request) = self.iconic.request.take() else {
+ close_flyout:: (ctx, self.id);
+ return;
+ };
+ let Some(active) = request.group.active_panel().cloned() else { return };
+ let id = self.id.with("flyout");
+ if self.iconic.auto_collapse && self.flyout_dismissed(ctx, id, request.column) {
+ close_flyout:: (ctx, self.id);
+ return;
+ }
+ let screen = ctx.content_rect();
+ let bounds = self.workspace.intersect(screen);
+ let width = request.width.max(self.style.min_pane).min(bounds.width());
+ let height = self
+ .content
+ .floating_size(&active)
+ .map(|size| size.y)
+ .or(request.group.height)
+ .filter(|h| h.is_finite() && *h > 0.0)
+ .unwrap_or(FLYOUT_HEIGHT)
+ .max(self.style.tab_height + self.style.min_pane)
+ .min(bounds.height());
+ let left = match request.edge {
+ Edge::Left => request.column.right(),
+ Edge::Right => request.column.left() - width,
+ };
+ let top = request.icons.top().min(bounds.bottom() - height).max(bounds.top());
+ let rect = Rect::from_min_size(pos2(left, top), vec2(width, height));
+ let style = ChromeStyle::from_dock(self.style);
+ let group = request.group;
+ let response = FloatingFrame::new(id, rect).bounds(bounds).resizable(false).show(ctx, &style, |ui, rect| {
+ self.group(ui, &group.panels, group.active, false, rect, GroupKind::Dock);
+ });
+ self.iconic.rect = Some(response.rect);
+ }
+
+ /// Escape (outside a drag) or a primary press outside the flyout, its column and any menu.
+ fn flyout_dismissed(&self, ctx: &Context, id: Id, column: Rect) -> bool {
+ let escape = !is_dragging:: (ctx, self.id) && ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::Escape));
+ let press = ctx.input(|i| i.pointer.primary_pressed().then(|| i.pointer.press_origin()).flatten());
+ let outside = press.is_some_and(|pos| {
+ let inside = ctx.memory(|m| m.area_rect(id)).is_some_and(|r| r.contains(pos)) || column.contains(pos);
+ let menu = ctx.layer_id_at(pos).is_some_and(|layer| matches!(layer.order, Order::Foreground | Order::Tooltip));
+ !inside && !menu
+ });
+ escape || outside
+ }
+}
diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs
index b8179c37..613bf689 100644
--- a/crates/ui/src/docking/render/mod.rs
+++ b/crates/ui/src/docking/render/mod.rs
@@ -9,6 +9,7 @@ use super::{Action, DockContent, DockOutput, DockStyle, DropTarget, Layout, Node
mod dock;
mod floating;
mod frame;
+mod iconic;
#[cfg(feature = "native-windows")]
mod native;
@@ -16,6 +17,9 @@ pub(in crate::docking) use frame::{FrameState, view};
pub(in crate::docking) use group::GroupKind;
+pub(in crate::docking) use iconic::Iconic;
+pub use iconic::{close_flyout, flyout, open_flyout};
+
pub(in crate::docking) use floating::RESIZE_HANDLES;
mod group;
mod preview;
@@ -35,6 +39,8 @@ pub(super) struct Renderer<'a, P> {
pub(super) owned_interactions: Vec ,
pub(super) frame: FrameState ,
+ /// [D] Flyout and iconic column state for this frame.
+ pub(super) iconic: Iconic ,
}
impl (panels: &[P], content: &dyn DockContent (node: &Node , style: &DockStyle, content: &dyn DockContent ) -> PanelLimits {
match node {
Node::Dock { columns, .. } => {
- let width: f32 = columns.iter().map(dock::column_width).sum();
+ let width: f32 = columns.iter().map(|c| dock::column_width(c, style)).sum();
PanelLimits { min: vec2(width.clamp(style.min_pane, 1_000_000.0), style.min_pane), ..PanelLimits::default() }
}
Node::Tabs { panels, .. } => {
diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs
index 6d982c88..734be291 100644
--- a/crates/ui/src/docking/render/preview.rs
+++ b/crates/ui/src/docking/render/preview.rs
@@ -269,7 +269,7 @@ impl {
/// Maximize Frame: maximize the frame holding `panel`, or restore it when it is the one
/// passed to [`DockArea::maximized`].
ToggleMaximize { panel: P },
+ /// [D] Auto-Collapse Iconic Panels was chosen in a column menu. The host flips its
+ /// preference and passes it to [`DockArea::auto_collapse_iconic`].
+ ToggleAutoCollapse,
}
/// Which panels [`DockArea::hide_panels`] hides. Protected panels always stay.
@@ -448,11 +471,19 @@ pub struct DockArea<'a, P = ()> {
blockers: Vec ,
hide: HidePanels<'a, P>,
+ auto_collapse: bool,
}
impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
pub fn new(id: Id) -> Self {
- Self { id, activation: None, blockers: Vec::new(), maximized: None, hide: HidePanels::None }
+ Self { id, activation: None, blockers: Vec::new(), maximized: None, hide: HidePanels::None, auto_collapse: true }
+ }
+
+ /// [D] Auto-Collapse Iconic Panels (default on): a flyout closes on Escape or a press
+ /// outside it. Off, it stays open until its icon or another icon is clicked.
+ pub fn auto_collapse_iconic(mut self, on: bool) -> Self {
+ self.auto_collapse = on;
+ self
}
/// Maximize Frame: the docked frame holding `panel` fills the area; other frames and
@@ -620,14 +651,17 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
owned_interactions: Vec::new(),
output: DockOutput::default(),
frame,
+ iconic: super::render::Iconic::new(self.auto_collapse),
};
if let Some(root) = &view.root {
renderer.node(ui, root, rect, &mut Vec::new());
ui.allocate_rect(rect, Sense::hover());
}
+ renderer.flyout(ui.ctx());
renderer.floating(ui.ctx(), &view.floating);
renderer.finish_group_drag(ui.ctx());
renderer.finish_drag(ui.ctx());
+ super::hits::record(ui.ctx(), self.id, &view, &renderer.owned_interactions);
ui.ctx().data_mut(|d| d.insert_temp(self.id.with("owned-interactions"), renderer.owned_interactions));
renderer.output
}
diff --git a/crates/ui/src/docking/testing.rs b/crates/ui/src/docking/testing.rs
new file mode 100644
index 00000000..64702470
--- /dev/null
+++ b/crates/ui/src/docking/testing.rs
@@ -0,0 +1,222 @@
+//! Drive a [`DockArea`] with real egui input in application tests (feature `testing`).
+//!
+//! ```ignore
+//! let mut driver = DockDriver::new(app_layout(), Panels::default());
+//! let bar = driver.hit(&Hit::DragBar(PanelId::Layers)).unwrap();
+//! driver.drag(bar.center(), egui::pos2(4.0, 300.0));
+//! assert!(driver.layout.dock_path(Side::Start).is_some());
+//! ```
+
+use std::{fmt::Debug, hash::Hash};
+
+use egui::{Context, Event, Id, Key, Modifiers, PointerButton, Pos2, RawInput, Rect, UiBuilder, epaint::ClippedShape, vec2};
+
+use super::{DockArea, DockContent, DockOutput, DockStyle, Hit, Layout};
+
+/// Pointer travel that starts a drag before moving to its destination.
+const NUDGE: egui::Vec2 = egui::Vec2::new(8.0, 6.0);
+
+type StyleFn = Box {
+ pub ctx: Context,
+ pub layout: Layout ,
+ pub content: C,
+ /// The area's id, for [`DockArea::hit_rects`] and the free docking functions.
+ pub id: Id,
+ /// The window, 800x600 by default.
+ pub screen: Rect,
+ /// Where the area draws inside the window; the whole window by default.
+ pub area: Rect,
+ /// Whether the window has focus. Losing it cancels gestures (C2).
+ pub focused: bool,
+ /// Modifiers held for the following events.
+ pub modifiers: Modifiers,
+ /// Passed to [`DockArea::auto_collapse_iconic`].
+ pub auto_collapse: bool,
+ /// Passed to [`DockArea::maximized`].
+ pub maximized: Option ,
+ /// Passed to [`DockArea::raise_panel`] on the next frame only.
+ pub raise: Option ,
+ style: Option {
+ /// A driver whose first frames have settled, so every control has its rect.
+ pub fn new(layout: Layout , content: C) -> Self {
+ let screen = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
+ let mut driver = Self {
+ ctx: Context::default(),
+ layout,
+ content,
+ id: Id::new("dock-driver"),
+ screen,
+ area: screen,
+ focused: true,
+ modifiers: Modifiers::NONE,
+ auto_collapse: true,
+ maximized: None,
+ raise: None,
+ style: None,
+ shapes: Vec::new(),
+ time: 0.0,
+ };
+ driver.settle();
+ driver
+ }
+
+ /// Adjust the style each frame, after [`DockStyle::from_ui`]. Settles again.
+ pub fn with_style(mut self, style: impl Fn(&mut DockStyle) + 'static) -> Self {
+ self.style = Some(Box::new(style));
+ self.settle();
+ self
+ }
+
+ /// Draw in `area` of the window. Settles again.
+ pub fn with_area(mut self, area: Rect) -> Self {
+ self.area = area;
+ self.settle();
+ self
+ }
+
+ /// Render one frame with `events` and return its output. Nothing is applied.
+ pub fn frame(&mut self, events: Vec {
+ let mut output = DockOutput::default();
+ self.time += 1.0 / 60.0;
+ let input = RawInput {
+ screen_rect: Some(self.screen),
+ events: std::iter::once(Event::ModifiersChanged(self.modifiers)).chain(events).collect(),
+ focused: self.focused,
+ time: Some(self.time),
+ ..Default::default()
+ };
+ let (layout, content, area, id) = (&self.layout, &mut self.content, self.area, self.id);
+ let (style_fn, maximized, raise, auto) = (&self.style, self.maximized.clone(), self.raise.take(), self.auto_collapse);
+ let mut full = self.ctx.run_ui(input, |ui| {
+ let mut style = DockStyle::from_ui(ui);
+ if let Some(adjust) = style_fn {
+ adjust(&mut style);
+ }
+ ui.scope_builder(UiBuilder::new().max_rect(area), |ui| {
+ let mut dock = DockArea::new(id).maximized(maximized.clone()).auto_collapse_iconic(auto);
+ if let Some(panel) = raise.clone() {
+ dock = dock.raise_panel(panel);
+ }
+ output = dock.show_content(ui, layout, &style, content);
+ });
+ });
+ full.textures_delta.clear();
+ self.shapes = full.shapes;
+ output
+ }
+
+ /// Render one frame and apply its actions, all or none, with the content's permissions. A
+ /// rejected batch is reported in the output's `error`.
+ pub fn step(&mut self, events: Vec {
+ let mut output = self.frame(events);
+ if output.error.is_none()
+ && let Err(error) = self.layout.apply_all(output.actions.clone(), |p| self.content.permissions(p))
+ {
+ output.error = Some(error);
+ }
+ output
+ }
+
+ /// Empty frames until moved widgets report their rects; egui reports them a frame late.
+ pub fn settle(&mut self) {
+ for _ in 0..3 {
+ self.step(Vec::new());
+ }
+ }
+
+ pub fn move_to(&mut self, pos: Pos2) -> DockOutput {
+ self.step(vec![Event::PointerMoved(pos)])
+ }
+
+ pub fn press(&mut self, pos: Pos2) -> DockOutput {
+ self.step(vec![Event::PointerMoved(pos), self.button(pos, true)])
+ }
+
+ pub fn release(&mut self, pos: Pos2) -> DockOutput {
+ self.step(vec![Event::PointerMoved(pos), self.button(pos, false)])
+ }
+
+ /// Press and release at `pos`; the output is the release frame's.
+ pub fn click(&mut self, pos: Pos2) -> DockOutput {
+ self.press(pos);
+ self.release(pos)
+ }
+
+ /// Press at `from`, move past the drag threshold and on to `to`, without releasing.
+ pub fn hold(&mut self, from: Pos2, to: Pos2) -> DockOutput {
+ self.press(from);
+ self.move_to(from + NUDGE);
+ self.move_to(to)
+ }
+
+ /// A whole drag; the output is the release frame's.
+ pub fn drag(&mut self, from: Pos2, to: Pos2) -> DockOutput {
+ self.hold(from, to);
+ self.release(to)
+ }
+
+ /// Press and release `key` in one frame.
+ pub fn key(&mut self, key: Key) -> DockOutput {
+ let modifiers = self.modifiers;
+ self.step(vec![
+ Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers },
+ Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers },
+ ])
+ }
+
+ /// Give or take the window's focus for this and the following frames.
+ pub fn focus(&mut self, focused: bool) -> DockOutput {
+ self.focused = focused;
+ self.step(vec![Event::WindowFocused(focused)])
+ }
+
+ /// The pointer leaves the window.
+ pub fn leave(&mut self) -> DockOutput {
+ self.step(vec![Event::PointerGone])
+ }
+
+ /// Every gesture source of the last frame.
+ pub fn hits(&self) -> Vec<(Hit , Rect)> {
+ DockArea:: ::hit_rects(&self.ctx, self.id)
+ }
+
+ /// The rect of one gesture source in the last frame.
+ pub fn hit(&self, hit: &Hit ) -> Option