forked from 1b-agent/opencadstudio-upstream
1200 lines
48 KiB
Rust
1200 lines
48 KiB
Rust
//! Dock drawing: the edges (icon strip and shown group), floating pallets,
|
||
//! the + menu and the drag preview.
|
||
|
||
use crate::app::document::DocumentTab;
|
||
use crate::app::{Message, OpenCADStudio};
|
||
use crate::t;
|
||
use crate::ui::dock::DOCK_DIVIDER_W;
|
||
use iced::widget::{button, canvas, column, container, mouse_area, row, stack, text, Space};
|
||
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
||
|
||
impl OpenCADStudio {
|
||
/// Everything the dock draws over the workspace (the row of edges and
|
||
/// viewport): floating pallets, an open + menu, the drag preview, and
|
||
/// the pointer capture that feeds drags.
|
||
pub(in crate::app) fn dock_overlay<'a>(
|
||
&'a self,
|
||
workspace: Element<'a, Message>,
|
||
tab: &'a DocumentTab,
|
||
) -> Element<'a, Message> {
|
||
// Floating panels sit over the workspace, back to front.
|
||
let mut layers: Vec<Element<'_, Message>> = vec![workspace];
|
||
for f in &self.dock.floating {
|
||
// Once a drag is under way only its preview shows the panel.
|
||
let dragged = self
|
||
.dock_drag
|
||
.and_then(|d| d.movement())
|
||
.is_some_and(|(panel, _, target)| panel == f.id && target.is_some());
|
||
if self.dock_panel_visible(f.id) && !dragged {
|
||
layers.push(self.floating_panel(*f, tab));
|
||
}
|
||
}
|
||
// An open pallet menu hangs beside its edge's + button, over a
|
||
// catcher that closes it on any click elsewhere.
|
||
if let Some(side) = self.dock_edge_menu {
|
||
if !self.dock_visible_groups(side).is_empty() {
|
||
// Checked = open and docked on this edge.
|
||
let checked: Vec<_> = crate::ui::dock::PanelId::ALL
|
||
.iter()
|
||
.map(|p| {
|
||
let here = self.dock.location(*p).map(|(s, _)| s) == Some(side);
|
||
(*p, here && self.dock_panel_visible(*p))
|
||
})
|
||
.collect();
|
||
let (_, plus_top) = self.dock_strip_layout(side);
|
||
let strip = crate::ui::dock::DOCK_STRIP_W;
|
||
let x = match side {
|
||
crate::app::config::DockSide::Left => strip + 4.0,
|
||
crate::app::config::DockSide::Right => {
|
||
self.dock_workspace_size().0 - strip - DOCK_MENU_W - 4.0
|
||
}
|
||
};
|
||
layers.push(
|
||
mouse_area(Space::new().width(Fill).height(Fill))
|
||
.on_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None)))
|
||
.on_right_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None)))
|
||
.into(),
|
||
);
|
||
layers.push(place_at(
|
||
dock_edge_menu(side, &checked),
|
||
x.max(0.0),
|
||
plus_top,
|
||
));
|
||
}
|
||
}
|
||
// An open floating title-bar menu, over its own click catcher.
|
||
if let Some(menu) = self.dock_float_menu_overlay() {
|
||
let close = Message::Dock(crate::ui::dock::DockMsg::FloatMenu(None));
|
||
layers.push(
|
||
mouse_area(Space::new().width(Fill).height(Fill))
|
||
.on_press(close.clone())
|
||
.on_right_press(close)
|
||
.into(),
|
||
);
|
||
layers.push(menu);
|
||
}
|
||
// While a panel is dragged, preview where it lands.
|
||
if let Some((id, _, Some(target))) = self.dock_drag.and_then(|d| d.movement()) {
|
||
layers.push(self.dock_drop_preview(id, target));
|
||
}
|
||
// Always a stack (and, below, always a mouse area), even with nothing
|
||
// on top: iced keeps a widget's state only while the tree keeps its
|
||
// shape, and a title bar's double-click needs its first click to
|
||
// survive the drag that pressing the bar starts.
|
||
let workspace: Element<'_, Message> = iced::widget::Stack::with_children(layers)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.into();
|
||
use crate::ui::dock::DockDrag;
|
||
let capturing =
|
||
self.dock_drag.is_some() || self.xref_col_drag.is_some() || self.xref_split_drag;
|
||
let mut capture = mouse_area(workspace);
|
||
if capturing {
|
||
capture = capture
|
||
.on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p)))
|
||
.on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease))
|
||
.interaction(match self.dock_drag {
|
||
Some(DockDrag::FloatSize {
|
||
from_left: true, ..
|
||
}) => iced::mouse::Interaction::ResizingDiagonallyUp,
|
||
Some(DockDrag::FloatSize {
|
||
from_left: false, ..
|
||
}) => iced::mouse::Interaction::ResizingDiagonallyDown,
|
||
Some(DockDrag::Split { .. }) => iced::mouse::Interaction::ResizingVertically,
|
||
Some(DockDrag::Move { .. }) => iced::mouse::Interaction::Grabbing,
|
||
// Widths, the Layer Manager column and the xref drags.
|
||
_ => iced::mouse::Interaction::ResizingHorizontally,
|
||
});
|
||
}
|
||
capture.into()
|
||
}
|
||
|
||
/// Build one edge: the vertical icon strip at the very edge plus the
|
||
/// edge's shown group beside it. The strip lists every group as a grip
|
||
/// bar over its pallets' icons, with a + button below; clicking an icon
|
||
/// shows its group. The shown group's pallets stack top to bottom, each
|
||
/// at its weighted share of the height, with a draggable splitter between
|
||
/// neighbours. A group whose pallets all auto-hide shows only while one
|
||
/// of its icons (or the group itself) is hovered; leaving the edge hides
|
||
/// it again. `None` when nothing on the edge is open.
|
||
pub(in crate::app) fn build_edge_stack<'a>(
|
||
&'a self,
|
||
side: crate::app::config::DockSide,
|
||
tab: &'a DocumentTab,
|
||
) -> Option<Element<'a, Message>> {
|
||
let gi = self.dock_shown_group(side)?;
|
||
let strip = self.dock_icon_strip(side);
|
||
let column_el = self.dock_edge_expanded(side).then(|| {
|
||
let width = self.dock_group_width(side, gi);
|
||
let group = &self.dock.groups(side)[gi];
|
||
let mut layers: Vec<Element<'_, Message>> = Vec::new();
|
||
let mut prev: Option<usize> = None;
|
||
for (pos, id) in group.panels.iter().enumerate() {
|
||
if !self.dock_panel_visible(*id) {
|
||
continue;
|
||
}
|
||
if let Some(upper) = prev {
|
||
layers.push(dock_splitter(side, gi, upper, pos, width));
|
||
}
|
||
layers.push(
|
||
container(self.expanded_panel(*id, side, width, tab))
|
||
.height(Length::FillPortion(crate::ui::dock::portion(
|
||
group.weights[pos],
|
||
)))
|
||
.clip(true)
|
||
.into(),
|
||
);
|
||
prev = Some(pos);
|
||
}
|
||
column(layers).height(Fill).into()
|
||
});
|
||
|
||
let mut children: Vec<Element<'_, Message>> = Vec::new();
|
||
match side {
|
||
crate::app::config::DockSide::Left => {
|
||
children.push(strip);
|
||
children.extend(column_el);
|
||
}
|
||
crate::app::config::DockSide::Right => {
|
||
children.extend(column_el);
|
||
children.push(strip);
|
||
}
|
||
}
|
||
Some(
|
||
mouse_area(row(children).height(Fill))
|
||
.on_enter(Message::Dock(crate::ui::dock::DockMsg::HoverStay))
|
||
.on_exit(Message::Dock(crate::ui::dock::DockMsg::HoverExit))
|
||
.into(),
|
||
)
|
||
}
|
||
|
||
/// An edge's vertical icon strip: per group its open pallets' icons in a
|
||
/// row ended by a hairline divider, then a + button opening the pallet
|
||
/// menu. The band along a group's window-side edge is its grip: it
|
||
/// carries the blue bar of the shown group, lights up while hovered (or
|
||
/// while it is a drop target) and drags the whole group; an icon shows its
|
||
/// group on click and drags just that pallet. Heights follow
|
||
/// `dock::strip_layout`, which drop targeting uses too.
|
||
fn dock_icon_strip(&self, side: crate::app::config::DockSide) -> Element<'_, Message> {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{
|
||
DockMsg, DropTarget, DOCK_STRIP_W, STRIP_CELL_H, STRIP_DIVIDER_H, STRIP_GRIP_W,
|
||
STRIP_PAD, STRIP_PLUS_GAP,
|
||
};
|
||
// Only a group actually on screen is marked as shown: on an
|
||
// auto-hiding edge nothing is, until a group is revealed.
|
||
let shown = self
|
||
.dock_shown_group(side)
|
||
.filter(|_| self.dock_edge_expanded(side));
|
||
let movement = self.dock_drag.and_then(|d| d.movement());
|
||
let move_target = movement.and_then(|(_, _, target)| target);
|
||
let dragging = move_target.is_some();
|
||
let target_group = match move_target {
|
||
Some(DropTarget::Join { side: s, group, .. }) if s == side => Some(group),
|
||
_ => None,
|
||
};
|
||
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
||
for gi in self.dock_visible_groups(side) {
|
||
let is_shown = shown == Some(gi);
|
||
let lit =
|
||
target_group == Some(gi) || (!dragging && self.dock_grip_hover == Some((side, gi)));
|
||
let group_dragged =
|
||
dragging && movement.is_some_and(|(_, group, _)| group == Some((side, gi)));
|
||
let panels = self.dock_group_visible(side, gi);
|
||
let row_h = 2.0 * STRIP_PAD + panels.len() as f32 * STRIP_CELL_H;
|
||
|
||
let mut icons: Vec<Element<'_, Message>> = vec![Space::new().height(STRIP_PAD).into()];
|
||
for id in panels {
|
||
let faded = group_dragged
|
||
|| (dragging
|
||
&& movement
|
||
.is_some_and(|(panel, group, _)| group.is_none() && panel == id));
|
||
let glyph = if faded {
|
||
crate::ui::icons::themed_disabled(id.icon(), 20.0)
|
||
} else if is_shown {
|
||
crate::ui::icons::themed(id.icon(), 20.0)
|
||
} else {
|
||
crate::ui::icons::themed_secondary(id.icon(), 20.0)
|
||
};
|
||
let hovered = !dragging && self.dock_icon_hover == Some(id);
|
||
let cell = container(glyph)
|
||
.center_x(Length::Fixed(STRIP_CELL_H))
|
||
.center_y(Length::Fixed(STRIP_CELL_H - 2.0))
|
||
.style(move |theme: &Theme| container::Style {
|
||
background: hovered
|
||
.then(|| Background::Color(theme.palette().background.weak.color)),
|
||
border: Border {
|
||
radius: 5.0.into(),
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
});
|
||
let cell = mouse_area(
|
||
container(cell)
|
||
.center_x(Length::Fixed(DOCK_STRIP_W))
|
||
.center_y(Length::Fixed(STRIP_CELL_H)),
|
||
)
|
||
.on_press(Message::Dock(DockMsg::IconPress(id)))
|
||
.on_enter(Message::Dock(DockMsg::Hover(id)))
|
||
.on_exit(Message::Dock(DockMsg::HoverEnd(id)))
|
||
.interaction(iced::mouse::Interaction::Pointer);
|
||
icons.push(cell.into());
|
||
}
|
||
|
||
// The group grip: a band on the window side holding the edge bar.
|
||
let bar_w = if lit { 5.0 } else { 3.0 };
|
||
let bar_inset = 5.0;
|
||
let bar = container(
|
||
Space::new()
|
||
.width(bar_w)
|
||
.height(Length::Fixed(row_h - 2.0 * bar_inset)),
|
||
)
|
||
.style(move |theme: &Theme| {
|
||
let palette = theme.palette();
|
||
let (color, glow) = if lit {
|
||
(Some(palette.primary.weak.color), true)
|
||
} else if is_shown {
|
||
(Some(palette.primary.base.color), false)
|
||
} else {
|
||
(None, false)
|
||
};
|
||
container::Style {
|
||
background: color.map(Background::Color),
|
||
border: Border {
|
||
radius: match side {
|
||
DockSide::Left => iced::border::Radius::default().right(3.0),
|
||
DockSide::Right => iced::border::Radius::default().left(3.0),
|
||
},
|
||
..Default::default()
|
||
},
|
||
shadow: if glow {
|
||
iced::Shadow {
|
||
color: palette.primary.base.color.scale_alpha(0.55),
|
||
offset: iced::Vector::new(0.0, 0.0),
|
||
blur_radius: 8.0,
|
||
}
|
||
} else {
|
||
iced::Shadow::default()
|
||
},
|
||
..Default::default()
|
||
}
|
||
});
|
||
let band = mouse_area(
|
||
container(bar)
|
||
.width(Length::Fixed(STRIP_GRIP_W))
|
||
.height(Length::Fixed(row_h))
|
||
.align_y(iced::alignment::Vertical::Center)
|
||
.align_x(match side {
|
||
DockSide::Left => iced::alignment::Horizontal::Left,
|
||
DockSide::Right => iced::alignment::Horizontal::Right,
|
||
}),
|
||
)
|
||
.on_press(Message::Dock(DockMsg::GroupGrab(side, gi)))
|
||
.on_enter(Message::Dock(DockMsg::GripHover(side, gi)))
|
||
.on_exit(Message::Dock(DockMsg::GripHoverEnd(side, gi)))
|
||
.interaction(iced::mouse::Interaction::Grab);
|
||
let band = container(band)
|
||
.width(Fill)
|
||
.height(Length::Fixed(row_h))
|
||
.align_x(match side {
|
||
DockSide::Left => iced::alignment::Horizontal::Left,
|
||
DockSide::Right => iced::alignment::Horizontal::Right,
|
||
});
|
||
items.push(
|
||
// A stack is as tall as its first layer, so the icon column
|
||
// spans the whole row; the edge bar then centres on the row.
|
||
container(stack![column(icons).height(Length::Fixed(row_h)), band])
|
||
.width(Length::Fixed(DOCK_STRIP_W))
|
||
.height(Length::Fixed(row_h))
|
||
.style(move |theme: &Theme| container::Style {
|
||
background: lit.then(|| {
|
||
Background::Color(theme.palette().primary.weak.color.scale_alpha(0.12))
|
||
}),
|
||
..Default::default()
|
||
})
|
||
.into(),
|
||
);
|
||
items.push(
|
||
container(Space::new())
|
||
.width(Fill)
|
||
.height(Length::Fixed(STRIP_DIVIDER_H))
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(
|
||
theme.palette().background.neutral.color,
|
||
)),
|
||
..Default::default()
|
||
})
|
||
.into(),
|
||
);
|
||
}
|
||
items.push(Space::new().height(STRIP_PLUS_GAP).into());
|
||
let plus = button(crate::ui::icons::themed_secondary(
|
||
crate::ui::icons::PLUS,
|
||
14.0,
|
||
))
|
||
.on_press(Message::Dock(DockMsg::EdgeMenu(Some(side))))
|
||
.style(|theme: &Theme, status| button::Style {
|
||
background: Some(Background::Color(match status {
|
||
button::Status::Hovered | button::Status::Pressed => {
|
||
theme.palette().background.weak.color
|
||
}
|
||
_ => theme.palette().background.weakest.color,
|
||
})),
|
||
border: Border {
|
||
radius: 14.0.into(),
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
})
|
||
.width(Length::Fixed(28.0))
|
||
.height(Length::Fixed(28.0))
|
||
.padding(7);
|
||
items.push(container(plus).center_x(Length::Fixed(DOCK_STRIP_W)).into());
|
||
container(column(items))
|
||
.width(Length::Fixed(DOCK_STRIP_W))
|
||
.height(Fill)
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||
border: Border {
|
||
color: theme.palette().background.neutral.color,
|
||
width: 1.0,
|
||
radius: 0.0.into(),
|
||
},
|
||
..Default::default()
|
||
})
|
||
.into()
|
||
}
|
||
|
||
/// The content of panel `id` at `width`. A `floating` panel leaves its
|
||
/// title to the floating frame's vertical title bar.
|
||
pub(in crate::app) fn panel_body<'a>(
|
||
&'a self,
|
||
id: crate::ui::dock::PanelId,
|
||
width: f32,
|
||
floating: bool,
|
||
tab: &'a DocumentTab,
|
||
) -> Element<'a, Message> {
|
||
let chrome = crate::ui::dock::Chrome {
|
||
auto_collapse: self.dock.auto_hides(id),
|
||
floating,
|
||
title_hovered: self.dock_title_hover == Some(id),
|
||
};
|
||
match id {
|
||
crate::ui::dock::PanelId::Properties => tab.properties.view(width, chrome),
|
||
crate::ui::dock::PanelId::GraphicAttributes => {
|
||
crate::ui::window::graphic_attributes::view(
|
||
&self.graphic_attributes,
|
||
&tab.scene,
|
||
&tab.properties,
|
||
width,
|
||
chrome,
|
||
self.dock
|
||
.location(id)
|
||
.map_or(crate::app::config::DockSide::Right, |(side, _)| side),
|
||
matches!(
|
||
self.color_pick_target,
|
||
Some((crate::app::ColorPickTarget::Gradient(_), _))
|
||
),
|
||
)
|
||
}
|
||
crate::ui::dock::PanelId::BlockPalette => {
|
||
crate::ui::window::block_palette::view(&self.block_palette, width, chrome)
|
||
}
|
||
crate::ui::dock::PanelId::ExternalReferences => {
|
||
self.xref_manager
|
||
.view(width, chrome, tab.xref_missing, &tab.scene.document)
|
||
}
|
||
crate::ui::dock::PanelId::Browser => crate::ui::window::browser::view(
|
||
&tab.scene.document,
|
||
tab.sketch_session
|
||
.as_ref()
|
||
.map(|session| session.name.as_str()),
|
||
width,
|
||
chrome,
|
||
),
|
||
crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, chrome),
|
||
crate::ui::dock::PanelId::Count => crate::ui::window::count_palette::view(
|
||
&self.count_palette,
|
||
tab.count.as_ref(),
|
||
&tab.scene.document,
|
||
tab.id,
|
||
tab.scene.geometry_epoch,
|
||
width,
|
||
chrome,
|
||
),
|
||
crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view(
|
||
&self.pc_manager,
|
||
&tab.scene.document,
|
||
&tab.scene.selected_handles_in_order(),
|
||
width,
|
||
chrome,
|
||
),
|
||
crate::ui::dock::PanelId::SheetSetManager => {
|
||
crate::ui::window::sheet_set::view(&self.sheet_set, width, chrome)
|
||
}
|
||
crate::ui::dock::PanelId::Layers => {
|
||
tab.layers.view_panel(self.layer_name_col_w, width, chrome)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A pallet of the shown group: its body plus a grabbable divider
|
||
/// against the viewport (sizing the whole group). Hovering is handled by
|
||
/// the enclosing edge region (see `build_edge_stack`), so the body stays
|
||
/// fully interactive without fighting the region's hover tracking.
|
||
fn expanded_panel<'a>(
|
||
&'a self,
|
||
id: crate::ui::dock::PanelId,
|
||
side: crate::app::config::DockSide,
|
||
width: f32,
|
||
tab: &'a DocumentTab,
|
||
) -> Element<'a, Message> {
|
||
let panel = self.panel_body(id, width, false, tab);
|
||
let divider = dock_divider(id);
|
||
match side {
|
||
crate::app::config::DockSide::Left => row![panel, divider].height(Fill).into(),
|
||
crate::app::config::DockSide::Right => row![divider, panel].height(Fill).into(),
|
||
}
|
||
}
|
||
|
||
/// A floating panel at its saved place: a vertical title bar on the side
|
||
/// facing the nearer workspace edge, the panel beside it, and a corner
|
||
/// grip to resize it. With its pin on, the panel hides down to the title
|
||
/// bar until hovered.
|
||
fn floating_panel<'a>(
|
||
&'a self,
|
||
f: crate::ui::dock::FloatPanel,
|
||
tab: &'a DocumentTab,
|
||
) -> Element<'a, Message> {
|
||
use crate::ui::dock::DockMsg;
|
||
let FloatFrame {
|
||
x,
|
||
y,
|
||
bar_left,
|
||
hidden,
|
||
} = self.dock_float_frame(f);
|
||
let hovered = self.dock_title_hover == Some(f.id);
|
||
let bar = floating_title_bar(f.id, self.dock.auto_collapse(f.id), bar_left, hovered);
|
||
let content: Element<'_, Message> = if hidden {
|
||
bar
|
||
} else {
|
||
// The resize grip sits in the bottom corner away from the bar.
|
||
let grip_left = !bar_left;
|
||
let grip_icon = crate::ui::icons::themed_secondary(
|
||
if grip_left {
|
||
crate::ui::icons::RESIZE_LEFT
|
||
} else {
|
||
crate::ui::icons::RESIZE
|
||
},
|
||
12.0,
|
||
);
|
||
let grip = mouse_area(
|
||
container(grip_icon)
|
||
.center_x(Length::Fixed(14.0))
|
||
.center_y(Length::Fixed(14.0)),
|
||
)
|
||
.on_press(Message::Dock(DockMsg::FloatResizeGrab(f.id, grip_left)))
|
||
.interaction(if grip_left {
|
||
iced::mouse::Interaction::ResizingDiagonallyUp
|
||
} else {
|
||
iced::mouse::Interaction::ResizingDiagonallyDown
|
||
});
|
||
let body = stack![
|
||
self.panel_body(f.id, f.w, true, tab),
|
||
container(grip)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.align_x(if grip_left {
|
||
iced::alignment::Horizontal::Left
|
||
} else {
|
||
iced::alignment::Horizontal::Right
|
||
})
|
||
.align_y(iced::alignment::Vertical::Bottom),
|
||
]
|
||
.width(Length::Fixed(f.w))
|
||
.height(Fill);
|
||
if bar_left {
|
||
row![bar, body].into()
|
||
} else {
|
||
row![body, bar].into()
|
||
}
|
||
};
|
||
// Clipped, so a panel squeezed below its content's height never
|
||
// draws outside its frame.
|
||
let framed = container(content)
|
||
.height(Length::Fixed(f.h))
|
||
.clip(true)
|
||
.style(|theme: &Theme| container::Style {
|
||
border: Border {
|
||
color: theme.palette().background.strong.color,
|
||
width: 1.0,
|
||
radius: 0.0.into(),
|
||
},
|
||
shadow: iced::Shadow {
|
||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
|
||
offset: iced::Vector::new(0.0, 4.0),
|
||
blur_radius: 14.0,
|
||
},
|
||
..Default::default()
|
||
});
|
||
// The idle cursor marks the panel as opaque to the pointer, so clicks
|
||
// on its empty areas don't fall through to the drawing below.
|
||
let panel = mouse_area(framed)
|
||
.on_press(Message::Dock(DockMsg::FloatRaise(f.id)))
|
||
.on_enter(Message::Dock(DockMsg::Hover(f.id)))
|
||
.on_exit(Message::Dock(DockMsg::HoverExit))
|
||
.interaction(iced::mouse::Interaction::Idle);
|
||
place_at(panel.into(), x, y)
|
||
}
|
||
|
||
/// Where floating panel `f` draws.
|
||
fn dock_float_frame(&self, f: crate::ui::dock::FloatPanel) -> FloatFrame {
|
||
let (ww, wh) = self.dock_workspace_size();
|
||
// The bar faces the nearer workspace edge; while resizing, it stays
|
||
// opposite the grip being dragged so it cannot flip mid-drag.
|
||
let bar_left = match self.dock_drag {
|
||
Some(crate::ui::dock::DockDrag::FloatSize { panel, from_left }) if panel == f.id => {
|
||
!from_left
|
||
}
|
||
_ => f.x + (f.w + DOCK_FLOAT_BAR_W) * 0.5 < ww * 0.5,
|
||
};
|
||
let hidden = self.dock.auto_collapse(f.id) && self.dock_peek != Some(f.id);
|
||
// A hidden panel keeps its title bar where it sits when shown.
|
||
let x = if hidden && !bar_left { f.x + f.w } else { f.x };
|
||
let frame = FloatFrame {
|
||
x,
|
||
y: f.y,
|
||
bar_left,
|
||
hidden,
|
||
};
|
||
// Keep the panel on screen when the window shrank since it was placed.
|
||
FloatFrame {
|
||
x: x.min(ww - frame.width(f)).max(0.0),
|
||
y: f.y.min(wh - f.h).max(0.0),
|
||
..frame
|
||
}
|
||
}
|
||
|
||
/// The open right-click menu of a floating title bar: Allow docking,
|
||
/// Dock left and Dock right. It opens at the pointer, flipped to stay on
|
||
/// screen.
|
||
fn dock_float_menu_overlay(&self) -> Option<Element<'_, Message>> {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::DockMsg;
|
||
let id = self.dock_float_menu?;
|
||
let f = self.dock.float_rect(id)?;
|
||
if !self.dock_panel_visible(id) {
|
||
return None;
|
||
}
|
||
let (ww, wh) = self.dock_workspace_size();
|
||
let frame = self.dock_float_frame(f);
|
||
// Where the right-click happened, in the workspace.
|
||
let at = iced::Point::new(
|
||
frame.bar_x(f) + self.dock_float_menu_at.x,
|
||
frame.y + self.dock_float_menu_at.y,
|
||
);
|
||
let menu_x = if at.x + DOCK_MENU_W <= ww {
|
||
at.x
|
||
} else {
|
||
at.x - DOCK_MENU_W
|
||
};
|
||
let menu_x = menu_x.clamp(0.0, (ww - DOCK_MENU_W).max(0.0));
|
||
let menu_y = at.y.min(wh - FLOAT_MENU_H).max(0.0);
|
||
let item =
|
||
|label: Element<'static, Message>, press: DockMsg| -> Element<'static, Message> {
|
||
button(label)
|
||
.on_press(Message::Dock(press))
|
||
.style(menu_row_style)
|
||
.padding([5, 8])
|
||
.width(Fill)
|
||
.into()
|
||
};
|
||
let check: Element<'static, Message> = if f.docking {
|
||
crate::ui::icons::themed(crate::ui::icons::CHECK, 12.0)
|
||
} else {
|
||
Space::new().width(12).height(12).into()
|
||
};
|
||
let docking =
|
||
row![text(t!("Allow docking")).size(12).width(Fill), check].align_y(iced::Center);
|
||
let separator = container(Space::new())
|
||
.width(Fill)
|
||
.height(Length::Fixed(1.0))
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
||
..Default::default()
|
||
});
|
||
let menu = column![
|
||
item(docking.into(), DockMsg::FloatDockingToggle(id)),
|
||
container(separator).padding([3, 4]),
|
||
item(
|
||
text(t!("Dock left")).size(12).into(),
|
||
DockMsg::DockTo(id, DockSide::Left)
|
||
),
|
||
item(
|
||
text(t!("Dock right")).size(12).into(),
|
||
DockMsg::DockTo(id, DockSide::Right)
|
||
),
|
||
]
|
||
.spacing(1)
|
||
.padding(4);
|
||
Some(place_at(menu_frame(menu.into()), menu_x, menu_y))
|
||
}
|
||
|
||
/// The live drop preview for dragged panel `id`: a tint over the target
|
||
/// edge plus a panel-sized ghost where it lands — stacked into a group
|
||
/// (sized as that group will be), as a new group of its own (full
|
||
/// height), or as a floating window.
|
||
fn dock_drop_preview(
|
||
&self,
|
||
id: crate::ui::dock::PanelId,
|
||
target: crate::ui::dock::DropTarget,
|
||
) -> Element<'_, Message> {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::DropTarget;
|
||
let (ww, _) = self.dock_workspace_size();
|
||
let strip = crate::ui::dock::DOCK_STRIP_W;
|
||
let tint = |side: DockSide, w: f32| -> Element<'_, Message> {
|
||
let band = container(Space::new())
|
||
.width(Length::Fixed(w + strip))
|
||
.height(Fill)
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(
|
||
theme.palette().primary.weak.color.scale_alpha(0.35),
|
||
)),
|
||
..Default::default()
|
||
});
|
||
container(band)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.align_x(match side {
|
||
DockSide::Left => iced::alignment::Horizontal::Left,
|
||
DockSide::Right => iced::alignment::Horizontal::Right,
|
||
})
|
||
.into()
|
||
};
|
||
let mut layers: Vec<Element<'_, Message>> = Vec::new();
|
||
let group_drag = self
|
||
.dock_drag
|
||
.and_then(|d| d.movement())
|
||
.and_then(|(_, g, _)| g);
|
||
let marks_side = match target {
|
||
DropTarget::Edge { side, .. } | DropTarget::Join { side, .. } => Some(side),
|
||
DropTarget::Float { x, y } => {
|
||
let (w, h) = self.dock_float_size(id);
|
||
layers.push(place_at(dock_ghost(id, w + DOCK_FLOAT_BAR_W, h), x, y));
|
||
None
|
||
}
|
||
};
|
||
// A single pallet also gets a ghost where it lands in the column; a
|
||
// whole group shows only the strip feedback, as the column keeps
|
||
// showing what is there.
|
||
if let (Some(side), None) = (marks_side, group_drag) {
|
||
if let Some((top, bottom, w)) = self.dock_landing(id, target) {
|
||
let x = match side {
|
||
DockSide::Left => strip,
|
||
DockSide::Right => (ww - w - strip).max(0.0),
|
||
};
|
||
layers.push(tint(side, w));
|
||
layers.push(place_at(dock_ghost(id, w, bottom - top), x, top));
|
||
}
|
||
}
|
||
// On top: the strip's drop marks and a ghost of what is dragged
|
||
// following the pointer.
|
||
if let Some(side) = marks_side {
|
||
layers.extend(self.dock_strip_drop_marks(side, target));
|
||
if let Some(p) = self.dock_drag_last {
|
||
let icons = match group_drag {
|
||
Some((s, g)) => self.dock_group_visible(s, g),
|
||
None => vec![id],
|
||
};
|
||
layers.push(place_at(dock_icon_ghost(&icons), p.x - 16.0, p.y - 15.0));
|
||
}
|
||
}
|
||
iced::widget::Stack::with_children(layers)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.into()
|
||
}
|
||
|
||
/// Where docked pallet `id` would sit after dropping on `target`: its
|
||
/// span (top, bottom) in its group as laid out after the drop, and the
|
||
/// group's width. `None` for a floating target.
|
||
fn dock_landing(
|
||
&self,
|
||
id: crate::ui::dock::PanelId,
|
||
target: crate::ui::dock::DropTarget,
|
||
) -> Option<(f32, f32, f32)> {
|
||
use crate::ui::dock::DropTarget;
|
||
let mut after = self.dock.clone();
|
||
match target {
|
||
DropTarget::Edge { side, index } => {
|
||
after.dock(id, side, index);
|
||
}
|
||
DropTarget::Join { side, group, index } => {
|
||
after.join_group(id, side, group, index);
|
||
}
|
||
DropTarget::Float { .. } => return None,
|
||
}
|
||
let (side, gi) = after.location(id)?;
|
||
let group = &after.groups(side)[gi];
|
||
let (_, top, bottom) = self
|
||
.dock_open_spans(group, Some(id))
|
||
.into_iter()
|
||
.find(|(i, _, _)| group.panels[*i] == id)?;
|
||
Some((top, bottom, after.group_width_px(side, gi, self.win_size.0)))
|
||
}
|
||
|
||
/// The icon strip's drop marks for `target` on `side`: a short line with
|
||
/// end caps between a group's icons (join that group there), or a
|
||
/// full-width line on a divider with a "+ New group" badge.
|
||
fn dock_strip_drop_marks(
|
||
&self,
|
||
side: crate::app::config::DockSide,
|
||
target: crate::ui::dock::DropTarget,
|
||
) -> Vec<Element<'_, Message>> {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DropTarget, DOCK_STRIP_W, STRIP_CELL_H};
|
||
let (ww, _) = self.dock_workspace_size();
|
||
let strip_x = match side {
|
||
DockSide::Left => 0.0,
|
||
DockSide::Right => ww - DOCK_STRIP_W,
|
||
};
|
||
let (layout, _) = self.dock_strip_layout(side);
|
||
let mut marks: Vec<Element<'_, Message>> = Vec::new();
|
||
match target {
|
||
DropTarget::Join { group, index, .. } => {
|
||
let Some(g) = layout.iter().find(|g| g.group == group) else {
|
||
return marks;
|
||
};
|
||
// Icons above the insertion point, counted as the strip shows
|
||
// them (the dragged icon still sits in its old place).
|
||
let dragged = self.dock_drag.and_then(|d| d.movement()).map(|(p, _, _)| p);
|
||
let above = self.dock.groups(side)[group].panels[..index]
|
||
.iter()
|
||
.filter(|p| self.dock_panel_visible(**p) || Some(**p) == dragged)
|
||
.count();
|
||
let y = g.icons_top + above as f32 * STRIP_CELL_H;
|
||
marks.push(place_at(dock_insert_line(), strip_x + 7.0, y - 3.5));
|
||
}
|
||
DropTarget::Edge { index, .. } => {
|
||
let y = layout
|
||
.iter()
|
||
.find(|g| g.group >= index)
|
||
.map(|g| g.top)
|
||
.or_else(|| layout.last().map(|g| g.bottom()))
|
||
.unwrap_or(0.0);
|
||
marks.push(place_at(
|
||
container(Space::new())
|
||
.width(Length::Fixed(DOCK_STRIP_W))
|
||
.height(Length::Fixed(4.0))
|
||
.style(dock_drop_line_style)
|
||
.into(),
|
||
strip_x,
|
||
(y - 2.0).max(0.0),
|
||
));
|
||
const BADGE_W: f32 = 96.0;
|
||
let badge = container(text(format!("+ {}", t!("New group"))).size(10))
|
||
.width(Length::Fixed(BADGE_W))
|
||
.center_x(Length::Fixed(BADGE_W))
|
||
.padding([2, 6])
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(theme.palette().primary.base.color)),
|
||
text_color: Some(theme.palette().primary.base.text),
|
||
border: Border {
|
||
radius: 9.0.into(),
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
});
|
||
let badge_x = match side {
|
||
DockSide::Left => DOCK_STRIP_W + 6.0,
|
||
DockSide::Right => ww - DOCK_STRIP_W - 6.0 - BADGE_W,
|
||
};
|
||
marks.push(place_at(badge.into(), badge_x, (y - 9.0).max(0.0)));
|
||
}
|
||
DropTarget::Float { .. } => {}
|
||
}
|
||
marks
|
||
}
|
||
}
|
||
|
||
/// The glowing accent used by the strip's drop lines.
|
||
fn dock_drop_line_style(theme: &Theme) -> container::Style {
|
||
container::Style {
|
||
background: Some(Background::Color(theme.palette().primary.base.color)),
|
||
border: Border {
|
||
radius: 2.0.into(),
|
||
..Default::default()
|
||
},
|
||
shadow: iced::Shadow {
|
||
color: theme.palette().primary.base.color.scale_alpha(0.8),
|
||
offset: iced::Vector::new(0.0, 0.0),
|
||
blur_radius: 6.0,
|
||
},
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// The short insertion line with end caps shown between a group's icons.
|
||
fn dock_insert_line() -> Element<'static, Message> {
|
||
let cap = || {
|
||
container(Space::new())
|
||
.width(Length::Fixed(3.0))
|
||
.height(Length::Fixed(7.0))
|
||
.style(dock_drop_line_style)
|
||
};
|
||
row![
|
||
cap(),
|
||
container(Space::new())
|
||
.width(Length::Fixed(20.0))
|
||
.height(Length::Fixed(3.0))
|
||
.style(dock_drop_line_style),
|
||
cap(),
|
||
]
|
||
.align_y(iced::Center)
|
||
.height(Length::Fixed(7.0))
|
||
.into()
|
||
}
|
||
|
||
/// The ghost following the pointer while pallets are dragged: their icons
|
||
/// in an accent-framed box.
|
||
fn dock_icon_ghost(icons: &[crate::ui::dock::PanelId]) -> Element<'static, Message> {
|
||
let glyphs: Vec<Element<'static, Message>> = icons
|
||
.iter()
|
||
.map(|id| {
|
||
container(crate::ui::icons::themed(id.icon(), 18.0))
|
||
.center_x(Length::Fixed(32.0))
|
||
.center_y(Length::Fixed(30.0))
|
||
.into()
|
||
})
|
||
.collect();
|
||
container(column(glyphs))
|
||
.style(|theme: &Theme| {
|
||
let palette = theme.palette();
|
||
container::Style {
|
||
background: Some(Background::Color(
|
||
palette.primary.base.color.scale_alpha(0.22),
|
||
)),
|
||
border: Border {
|
||
color: palette.primary.base.color,
|
||
width: 1.5,
|
||
radius: 6.0.into(),
|
||
},
|
||
..Default::default()
|
||
}
|
||
})
|
||
.into()
|
||
}
|
||
|
||
/// Canvas that draws a floating title bar's label, reading upward from the
|
||
/// bar's bottom end.
|
||
struct VBarLabel {
|
||
text: String,
|
||
}
|
||
|
||
impl canvas::Program<Message> for VBarLabel {
|
||
type State = ();
|
||
|
||
fn draw(
|
||
&self,
|
||
_state: &(),
|
||
renderer: &iced::Renderer,
|
||
theme: &Theme,
|
||
bounds: iced::Rectangle,
|
||
_cursor: iced::advanced::mouse::Cursor,
|
||
) -> Vec<canvas::Geometry> {
|
||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||
frame.with_save(|frame| {
|
||
frame.translate(iced::Vector::new(bounds.width / 2.0, bounds.height / 2.0));
|
||
frame.rotate(iced::Radians(-std::f32::consts::FRAC_PI_2));
|
||
// After the rotation the text runs along local +x (upward), so
|
||
// the bar's bottom end sits at -height/2.
|
||
frame.fill_text(canvas::Text {
|
||
content: self.text.clone(),
|
||
position: iced::Point::new(-bounds.height / 2.0, 0.0),
|
||
color: theme.palette().background.base.text.scale_alpha(0.72),
|
||
size: iced::Pixels(13.0),
|
||
align_x: iced::advanced::text::Alignment::Left,
|
||
align_y: iced::alignment::Vertical::Center,
|
||
shaping: iced::advanced::text::Shaping::Advanced,
|
||
..Default::default()
|
||
});
|
||
});
|
||
vec![frame.into_geometry()]
|
||
}
|
||
}
|
||
|
||
/// Width of a floating panel's vertical title bar.
|
||
const DOCK_FLOAT_BAR_W: f32 = 28.0;
|
||
|
||
/// Where a floating panel draws: its top-left corner, whether its title bar
|
||
/// is on the left, and whether it is hidden down to that bar (auto-hide, not
|
||
/// hovered).
|
||
#[derive(Debug, Clone, Copy)]
|
||
struct FloatFrame {
|
||
x: f32,
|
||
y: f32,
|
||
bar_left: bool,
|
||
hidden: bool,
|
||
}
|
||
|
||
impl FloatFrame {
|
||
/// On-screen width of panel `f` in this frame: the bar, plus the panel
|
||
/// unless hidden.
|
||
fn width(&self, f: crate::ui::dock::FloatPanel) -> f32 {
|
||
if self.hidden {
|
||
DOCK_FLOAT_BAR_W
|
||
} else {
|
||
f.w + DOCK_FLOAT_BAR_W
|
||
}
|
||
}
|
||
|
||
/// Left edge of the title bar.
|
||
fn bar_x(&self, f: crate::ui::dock::FloatPanel) -> f32 {
|
||
if self.bar_left {
|
||
self.x
|
||
} else {
|
||
self.x + self.width(f) - DOCK_FLOAT_BAR_W
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A floating panel's vertical title bar: close then the hide (auto-collapse)
|
||
/// pin at the top, the panel's icon at the bottom with its title reading
|
||
/// upward above it. Pressing the bar's free area drags the panel.
|
||
fn floating_title_bar(
|
||
id: crate::ui::dock::PanelId,
|
||
auto_collapse: bool,
|
||
on_left: bool,
|
||
hovered: bool,
|
||
) -> Element<'static, Message> {
|
||
use crate::ui::dock::DockMsg;
|
||
let tip = if on_left {
|
||
iced::widget::tooltip::Position::Left
|
||
} else {
|
||
iced::widget::tooltip::Position::Right
|
||
};
|
||
let title = canvas(VBarLabel {
|
||
text: t!(id.title()).into_owned(),
|
||
})
|
||
.width(Fill)
|
||
.height(Fill);
|
||
// Close and pin only show while the pointer is over the bar, as on a
|
||
// docked title bar.
|
||
let mut bar = column![].spacing(2);
|
||
if hovered {
|
||
bar = bar
|
||
.push(crate::ui::dock::close_button(id, tip))
|
||
.push(crate::ui::dock::pin_button(id, auto_collapse, tip));
|
||
}
|
||
let bar = bar
|
||
.push(container(title).width(Fill).height(Fill).padding([6, 0]))
|
||
.push(container(crate::ui::icons::themed(id.icon(), 16.0)).center_x(Fill))
|
||
.padding([4, 0])
|
||
.align_x(iced::Center)
|
||
.width(Length::Fixed(DOCK_FLOAT_BAR_W))
|
||
.height(Fill);
|
||
mouse_area(container(bar).style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||
..Default::default()
|
||
}))
|
||
.on_press(Message::Dock(DockMsg::DockGrab(id)))
|
||
.on_right_press(Message::Dock(DockMsg::FloatMenu(Some(id))))
|
||
.on_enter(Message::Dock(DockMsg::TitleHover(id)))
|
||
.on_move(|p| Message::Dock(DockMsg::FloatBarPointer(p)))
|
||
.on_exit(Message::Dock(DockMsg::TitleHoverEnd(id)))
|
||
// Double-click docks it against the edge its title bar faces.
|
||
.on_double_click(Message::Dock(DockMsg::DockTo(
|
||
id,
|
||
if on_left {
|
||
crate::app::config::DockSide::Left
|
||
} else {
|
||
crate::app::config::DockSide::Right
|
||
},
|
||
)))
|
||
.interaction(iced::mouse::Interaction::Grab)
|
||
.into()
|
||
}
|
||
|
||
/// Grabbable separator between a docked pallet and the viewport: drag to
|
||
/// size its group, double-click to reset the width.
|
||
fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
|
||
let line = container(Space::new())
|
||
.width(Length::Fixed(DOCK_DIVIDER_W))
|
||
.height(Fill)
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
||
..Default::default()
|
||
});
|
||
let grab = Message::Dock(crate::ui::dock::DockMsg::ResizeGrab(id));
|
||
let reset = Message::Dock(crate::ui::dock::DockMsg::WidthReset(id));
|
||
mouse_area(line)
|
||
.on_press(grab)
|
||
.on_double_click(reset)
|
||
.interaction(iced::mouse::Interaction::ResizingHorizontally)
|
||
.into()
|
||
}
|
||
|
||
/// Width of the icon strip's pallet menu.
|
||
const DOCK_MENU_W: f32 = 230.0;
|
||
/// Rough height of a floating title bar's menu, to keep it on screen.
|
||
const FLOAT_MENU_H: f32 = 110.0;
|
||
|
||
/// The pallet menu of an edge's + button, styled like the right-click menu:
|
||
/// one row per pallet with its icon, its name and a check mark when it is
|
||
/// open on this edge. `checked` holds each pallet with that state.
|
||
fn dock_edge_menu(
|
||
side: crate::app::config::DockSide,
|
||
checked: &[(crate::ui::dock::PanelId, bool)],
|
||
) -> Element<'static, Message> {
|
||
use crate::ui::dock::DockMsg;
|
||
let rows: Vec<Element<'static, Message>> = checked
|
||
.iter()
|
||
.map(|(id, checked)| {
|
||
let id = *id;
|
||
let check: Element<'static, Message> = if *checked {
|
||
crate::ui::icons::themed(crate::ui::icons::CHECK, 12.0)
|
||
} else {
|
||
Space::new().width(12).height(12).into()
|
||
};
|
||
button(
|
||
row![
|
||
crate::ui::icons::themed(id.icon(), 16.0),
|
||
text(t!(id.title())).size(12).width(Fill),
|
||
check,
|
||
]
|
||
.spacing(8)
|
||
.align_y(iced::Center),
|
||
)
|
||
.on_press(Message::Dock(DockMsg::EdgeMenuToggle(side, id)))
|
||
.style(menu_row_style)
|
||
.padding([5, 8])
|
||
.width(Fill)
|
||
.into()
|
||
})
|
||
.collect();
|
||
menu_frame(column(rows).spacing(1).padding(4).into())
|
||
}
|
||
|
||
/// A menu row button: transparent until hovered.
|
||
fn menu_row_style(theme: &Theme, status: button::Status) -> button::Style {
|
||
let palette = theme.palette();
|
||
button::Style {
|
||
background: matches!(status, button::Status::Hovered | button::Status::Pressed)
|
||
.then(|| Background::Color(palette.background.weak.color)),
|
||
text_color: palette.background.base.text,
|
||
border: Border {
|
||
radius: 3.0.into(),
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// The raised box a dock menu sits in.
|
||
fn menu_frame(rows: Element<'static, Message>) -> Element<'static, Message> {
|
||
container(rows)
|
||
.width(Length::Fixed(DOCK_MENU_W))
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||
border: Border {
|
||
color: theme.palette().background.neutral.color,
|
||
width: 1.0,
|
||
radius: 4.0.into(),
|
||
},
|
||
shadow: iced::Shadow {
|
||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
|
||
offset: iced::Vector::new(0.0, 4.0),
|
||
blur_radius: 12.0,
|
||
},
|
||
..Default::default()
|
||
})
|
||
.into()
|
||
}
|
||
|
||
/// Draggable bar between two stacked pallets of group `gi`; double-click
|
||
/// gives every pallet of the group the same height again. It spans exactly
|
||
/// the pallets' width (panel plus its divider): a `Fill` width would stretch
|
||
/// the whole edge column across the workspace and squeeze the drawing view.
|
||
fn dock_splitter(
|
||
side: crate::app::config::DockSide,
|
||
gi: usize,
|
||
upper: usize,
|
||
lower: usize,
|
||
col_w: f32,
|
||
) -> Element<'static, Message> {
|
||
let line = container(Space::new())
|
||
.width(Length::Fixed(col_w + DOCK_DIVIDER_W))
|
||
.height(Length::Fixed(5.0))
|
||
.style(|theme: &Theme| container::Style {
|
||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
||
..Default::default()
|
||
});
|
||
mouse_area(line)
|
||
.on_press(Message::Dock(crate::ui::dock::DockMsg::SplitGrab(
|
||
side, gi, upper, lower,
|
||
)))
|
||
.on_double_click(Message::Dock(crate::ui::dock::DockMsg::SplitReset(
|
||
side, gi,
|
||
)))
|
||
.interaction(iced::mouse::Interaction::ResizingVertically)
|
||
.into()
|
||
}
|
||
|
||
/// The blue drop ghost: a `w`×`h` outline of where pallet `id` lands, with
|
||
/// its name as the header.
|
||
fn dock_ghost(id: crate::ui::dock::PanelId, w: f32, h: f32) -> Element<'static, Message> {
|
||
let header = container(
|
||
text(t!(id.title()))
|
||
.size(12)
|
||
.wrapping(iced::widget::text::Wrapping::None)
|
||
.ellipsis(iced::advanced::text::Ellipsis::End),
|
||
)
|
||
.width(Fill)
|
||
.padding([5, 8])
|
||
.clip(true)
|
||
.style(|theme: &Theme| {
|
||
let palette = theme.palette();
|
||
container::Style {
|
||
background: Some(Background::Color(palette.primary.base.color)),
|
||
text_color: Some(palette.primary.base.text),
|
||
..Default::default()
|
||
}
|
||
});
|
||
container(column![header, Space::new()])
|
||
.width(Length::Fixed(w.max(1.0)))
|
||
.height(Length::Fixed(h.max(1.0)))
|
||
.clip(true)
|
||
.style(|theme: &Theme| {
|
||
let palette = theme.palette();
|
||
container::Style {
|
||
background: Some(Background::Color(
|
||
palette.background.base.color.scale_alpha(0.85),
|
||
)),
|
||
border: Border {
|
||
color: palette.primary.base.color,
|
||
width: 2.0,
|
||
radius: 0.0.into(),
|
||
},
|
||
..Default::default()
|
||
}
|
||
})
|
||
.into()
|
||
}
|
||
|
||
/// Position `content` with its top-left corner at workspace point (`x`, `y`).
|
||
fn place_at(content: Element<'_, Message>, x: f32, y: f32) -> Element<'_, Message> {
|
||
container(content)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.align_x(iced::alignment::Horizontal::Left)
|
||
.align_y(iced::alignment::Vertical::Top)
|
||
.padding(iced::Padding {
|
||
top: y.max(0.0),
|
||
right: 0.0,
|
||
bottom: 0.0,
|
||
left: x.max(0.0),
|
||
})
|
||
.into()
|
||
}
|