feat(dock): stacked edge rendering and drag-to-reorder preview
This commit is contained in:
parent
10a1d89f19
commit
7bfb617745
8 changed files with 326 additions and 448 deletions
|
|
@ -27,11 +27,8 @@ pub struct AppConfig {
|
||||||
pub start: StartConfig,
|
pub start: StartConfig,
|
||||||
/// Which status-bar pills the user has hidden.
|
/// Which status-bar pills the user has hidden.
|
||||||
pub statusbar: StatusBarConfig,
|
pub statusbar: StatusBarConfig,
|
||||||
/// Dock position, width and auto-collapse behavior of the Properties panel.
|
|
||||||
pub properties: PropertiesDockConfig,
|
|
||||||
/// General edge-stack dock layout (which panels are docked, side, order,
|
/// General edge-stack dock layout (which panels are docked, side, order,
|
||||||
/// width and auto-collapse). Newer dock state; the Properties dock is
|
/// width and auto-collapse) for the Properties panel and block palette.
|
||||||
/// folded into this when its panel is migrated.
|
|
||||||
pub dock: crate::ui::dock::DockState,
|
pub dock: crate::ui::dock::DockState,
|
||||||
/// Add a newly selected annotation scale to existing annotative objects.
|
/// Add a newly selected annotation scale to existing annotative objects.
|
||||||
pub annotation_auto_scale: i8,
|
pub annotation_auto_scale: i8,
|
||||||
|
|
@ -52,7 +49,6 @@ impl Default for AppConfig {
|
||||||
recent: RecentConfig::default(),
|
recent: RecentConfig::default(),
|
||||||
start: StartConfig::default(),
|
start: StartConfig::default(),
|
||||||
statusbar: StatusBarConfig::default(),
|
statusbar: StatusBarConfig::default(),
|
||||||
properties: PropertiesDockConfig::default(),
|
|
||||||
dock: crate::ui::dock::DockState::default(),
|
dock: crate::ui::dock::DockState::default(),
|
||||||
annotation_auto_scale: -4,
|
annotation_auto_scale: -4,
|
||||||
ribbon: RibbonConfig::default(),
|
ribbon: RibbonConfig::default(),
|
||||||
|
|
@ -82,24 +78,6 @@ pub enum DockSide {
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct PropertiesDockConfig {
|
|
||||||
pub side: DockSide,
|
|
||||||
pub width: f32,
|
|
||||||
pub auto_collapse: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PropertiesDockConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
side: DockSide::Left,
|
|
||||||
width: 250.0,
|
|
||||||
auto_collapse: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct UiThemeConfig {
|
pub struct UiThemeConfig {
|
||||||
|
|
|
||||||
|
|
@ -340,19 +340,6 @@ pub(super) struct OpenCADStudio {
|
||||||
/// Widest natural single-row width of the Start-page action buttons,
|
/// Widest natural single-row width of the Start-page action buttons,
|
||||||
/// measured by `WrapFlow` so side lists collapse before those buttons wrap.
|
/// measured by `WrapFlow` so side lists collapse before those buttons wrap.
|
||||||
start_action_w: std::sync::Arc<std::sync::atomic::AtomicU32>,
|
start_action_w: std::sync::Arc<std::sync::atomic::AtomicU32>,
|
||||||
/// When the window is too narrow the properties panel collapses to a
|
|
||||||
/// vertical bar; this is the user's toggle to expand it back out.
|
|
||||||
props_expanded: bool,
|
|
||||||
/// Persisted dock width and side of the Properties panel.
|
|
||||||
properties_width: f32,
|
|
||||||
properties_side: config::DockSide,
|
|
||||||
properties_auto_collapse: bool,
|
|
||||||
/// Transient hover/drag state for the docked Properties panel.
|
|
||||||
properties_hovered: bool,
|
|
||||||
properties_dragging: bool,
|
|
||||||
properties_resizing: bool,
|
|
||||||
properties_drag_last: Option<Point>,
|
|
||||||
properties_dock_preview: Option<config::DockSide>,
|
|
||||||
/// Read-only editor buffer backing the command-line history dropdown, so
|
/// Read-only editor buffer backing the command-line history dropdown, so
|
||||||
/// the log can be drag-selected across lines and copied (issue #232).
|
/// the log can be drag-selected across lines and copied (issue #232).
|
||||||
/// Rebuilt from the history each time the dropdown is opened.
|
/// Rebuilt from the history each time the dropdown is opened.
|
||||||
|
|
@ -1771,23 +1758,6 @@ pub enum Message {
|
||||||
OpenUrl(String),
|
OpenUrl(String),
|
||||||
/// Select which section a narrow (tabbed) Start page shows.
|
/// Select which section a narrow (tabbed) Start page shows.
|
||||||
StartSectionSelect(StartSection),
|
StartSectionSelect(StartSection),
|
||||||
/// Expand/collapse the properties panel when it has shrunk to a bar.
|
|
||||||
TogglePropertiesBar,
|
|
||||||
/// Close the docked Properties panel; the ribbon command can reopen it.
|
|
||||||
PropertiesClose,
|
|
||||||
/// Pin/unpin the docked Properties panel.
|
|
||||||
PropertiesAutoCollapseToggle,
|
|
||||||
/// Hover state drives expansion while auto-collapse is enabled.
|
|
||||||
PropertiesHover(bool),
|
|
||||||
/// Begin dragging the Properties title bar to the opposite dock edge.
|
|
||||||
PropertiesDockGrab,
|
|
||||||
/// Begin dragging the Properties/viewport divider.
|
|
||||||
PropertiesResizeGrab,
|
|
||||||
/// Reset the dock width to its default value.
|
|
||||||
PropertiesWidthReset,
|
|
||||||
/// Full-workspace pointer tracking shared by dock and resize drags.
|
|
||||||
PropertiesDragMove(Point),
|
|
||||||
PropertiesDragRelease,
|
|
||||||
/// Scroll the status-bar layout-tab strip horizontally by `delta` px
|
/// Scroll the status-bar layout-tab strip horizontally by `delta` px
|
||||||
/// (negative = left). Driven by the ‹ › arrows next to the tabs.
|
/// (negative = left). Driven by the ‹ › arrows next to the tabs.
|
||||||
ScrollLayoutTabs(f32),
|
ScrollLayoutTabs(f32),
|
||||||
|
|
@ -3072,15 +3042,6 @@ impl OpenCADStudio {
|
||||||
props_asym_scale: std::collections::HashSet::new(),
|
props_asym_scale: std::collections::HashSet::new(),
|
||||||
start_section: StartSection::default(),
|
start_section: StartSection::default(),
|
||||||
start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||||
props_expanded: false,
|
|
||||||
properties_width: 250.0,
|
|
||||||
properties_side: config::DockSide::Left,
|
|
||||||
properties_auto_collapse: false,
|
|
||||||
properties_hovered: false,
|
|
||||||
properties_dragging: false,
|
|
||||||
properties_resizing: false,
|
|
||||||
properties_drag_last: None,
|
|
||||||
properties_dock_preview: None,
|
|
||||||
history_content: iced::widget::text_editor::Content::new(),
|
history_content: iced::widget::text_editor::Content::new(),
|
||||||
command_history_resizing: false,
|
command_history_resizing: false,
|
||||||
command_history_drag_last: None,
|
command_history_drag_last: None,
|
||||||
|
|
|
||||||
|
|
@ -599,7 +599,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||||
self.show_block_palette = false;
|
self.show_block_palette = false;
|
||||||
self.block_palette.placing = None;
|
self.block_palette.placing = None;
|
||||||
}
|
}
|
||||||
PanelId::Properties => self.show_properties = false,
|
PanelId::Properties => {
|
||||||
|
self.show_properties = false;
|
||||||
|
self.ribbon.set_properties(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if self.dock_expanded == Some(id) {
|
if self.dock_expanded == Some(id) {
|
||||||
self.dock_expanded = None;
|
self.dock_expanded = None;
|
||||||
|
|
@ -613,13 +616,20 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
}
|
}
|
||||||
DockMsg::Hover(id) => {
|
DockMsg::Hover(id) => {
|
||||||
self.dock_expanded = Some(id);
|
// Ignored while dragging/resizing: the pointer is over the
|
||||||
|
// drag preview, not a rail, so a hover must not collapse the
|
||||||
|
// panel being dragged or repoint the resize target.
|
||||||
|
if self.dock_dragging.is_none() && self.dock_resizing.is_none() {
|
||||||
|
self.dock_expanded = Some(id);
|
||||||
|
}
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
}
|
}
|
||||||
DockMsg::HoverExit => {
|
DockMsg::HoverExit => {
|
||||||
if let Some(id) = self.dock_expanded {
|
if self.dock_dragging.is_none() && self.dock_resizing.is_none() {
|
||||||
if self.dock.auto_collapse(id) {
|
if let Some(id) = self.dock_expanded {
|
||||||
self.dock_expanded = None;
|
if self.dock.auto_collapse(id) {
|
||||||
|
self.dock_expanded = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
|
|
|
||||||
|
|
@ -553,11 +553,6 @@ impl OpenCADStudio {
|
||||||
section: self.start_section,
|
section: self.start_section,
|
||||||
},
|
},
|
||||||
statusbar: self.statusbar_config.clone(),
|
statusbar: self.statusbar_config.clone(),
|
||||||
properties: crate::app::config::PropertiesDockConfig {
|
|
||||||
side: self.properties_side,
|
|
||||||
width: self.properties_width,
|
|
||||||
auto_collapse: self.properties_auto_collapse,
|
|
||||||
},
|
|
||||||
dock: {
|
dock: {
|
||||||
let mut dock = self.dock.clone();
|
let mut dock = self.dock.clone();
|
||||||
dock.ensure_settings();
|
dock.ensure_settings();
|
||||||
|
|
@ -600,13 +595,6 @@ impl OpenCADStudio {
|
||||||
// (`refresh_recent_thumbs`) — never here on the boot path.
|
// (`refresh_recent_thumbs`) — never here on the boot path.
|
||||||
self.start_section = cfg.start.section;
|
self.start_section = cfg.start.section;
|
||||||
self.statusbar_config = cfg.statusbar;
|
self.statusbar_config = cfg.statusbar;
|
||||||
self.properties_side = cfg.properties.side;
|
|
||||||
self.properties_width = if cfg.properties.width.is_finite() {
|
|
||||||
cfg.properties.width.clamp(220.0, 600.0)
|
|
||||||
} else {
|
|
||||||
250.0
|
|
||||||
};
|
|
||||||
self.properties_auto_collapse = cfg.properties.auto_collapse;
|
|
||||||
let mut dock = cfg.dock;
|
let mut dock = cfg.dock;
|
||||||
dock.ensure_settings();
|
dock.ensure_settings();
|
||||||
self.dock = dock;
|
self.dock = dock;
|
||||||
|
|
|
||||||
|
|
@ -377,115 +377,6 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::TogglePropertiesBar => {
|
|
||||||
self.props_expanded = !self.props_expanded;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesClose => {
|
|
||||||
self.show_properties = false;
|
|
||||||
self.properties_hovered = false;
|
|
||||||
self.properties_dragging = false;
|
|
||||||
self.properties_resizing = false;
|
|
||||||
self.properties_drag_last = None;
|
|
||||||
self.properties_dock_preview = None;
|
|
||||||
self.ribbon.set_properties(false);
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesAutoCollapseToggle => {
|
|
||||||
self.properties_auto_collapse ^= true;
|
|
||||||
// Keep the panel open while the pointer is still over the
|
|
||||||
// button; leaving the panel performs the first collapse.
|
|
||||||
self.properties_hovered = self.properties_auto_collapse;
|
|
||||||
self.save_config();
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesHover(hovered) => {
|
|
||||||
if self.properties_auto_collapse
|
|
||||||
&& !self.properties_dragging
|
|
||||||
&& !self.properties_resizing
|
|
||||||
{
|
|
||||||
self.properties_hovered = hovered;
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesDockGrab => {
|
|
||||||
self.properties_dragging = true;
|
|
||||||
self.properties_resizing = false;
|
|
||||||
self.properties_drag_last = None;
|
|
||||||
self.properties_dock_preview = Some(self.properties_side);
|
|
||||||
self.properties_hovered = true;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesResizeGrab => {
|
|
||||||
self.properties_resizing = true;
|
|
||||||
self.properties_dragging = false;
|
|
||||||
self.properties_drag_last = None;
|
|
||||||
self.properties_dock_preview = None;
|
|
||||||
self.properties_hovered = true;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesWidthReset => {
|
|
||||||
self.properties_width = 250.0;
|
|
||||||
self.save_config();
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesDragMove(point) => {
|
|
||||||
if self.properties_dragging {
|
|
||||||
self.properties_dock_preview = Some(if point.x < self.win_size.0 * 0.5 {
|
|
||||||
crate::app::config::DockSide::Left
|
|
||||||
} else {
|
|
||||||
crate::app::config::DockSide::Right
|
|
||||||
});
|
|
||||||
} else if self.properties_resizing {
|
|
||||||
if let Some(last) = self.properties_drag_last {
|
|
||||||
let dx = point.x - last.x;
|
|
||||||
let delta = match self.properties_side {
|
|
||||||
crate::app::config::DockSide::Left => dx,
|
|
||||||
crate::app::config::DockSide::Right => -dx,
|
|
||||||
};
|
|
||||||
let max_width = (self.win_size.0 * 0.45).clamp(220.0, 600.0);
|
|
||||||
self.properties_width =
|
|
||||||
(self.properties_width + delta).clamp(220.0, max_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.properties_dragging || self.properties_resizing {
|
|
||||||
self.properties_drag_last = Some(point);
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::PropertiesDragRelease => {
|
|
||||||
let changed = if self.properties_dragging {
|
|
||||||
if let Some(side) = self.properties_dock_preview {
|
|
||||||
let changed = side != self.properties_side;
|
|
||||||
self.properties_side = side;
|
|
||||||
changed
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.properties_resizing
|
|
||||||
};
|
|
||||||
self.properties_dragging = false;
|
|
||||||
self.properties_resizing = false;
|
|
||||||
self.properties_drag_last = None;
|
|
||||||
self.properties_dock_preview = None;
|
|
||||||
if self.properties_auto_collapse {
|
|
||||||
self.properties_hovered = false;
|
|
||||||
}
|
|
||||||
if changed {
|
|
||||||
self.save_config();
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::ScrollLayoutTabs(dx) => iced::widget::operation::scroll_by(
|
Message::ScrollLayoutTabs(dx) => iced::widget::operation::scroll_by(
|
||||||
iced::advanced::widget::Id::new(crate::ui::statusbar::LAYOUT_TABS_SCROLL_ID),
|
iced::advanced::widget::Id::new(crate::ui::statusbar::LAYOUT_TABS_SCROLL_ID),
|
||||||
iced::widget::scrollable::AbsoluteOffset { x: dx, y: 0.0 },
|
iced::widget::scrollable::AbsoluteOffset { x: dx, y: 0.0 },
|
||||||
|
|
|
||||||
|
|
@ -1458,92 +1458,36 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Docked Properties panel. It keeps its pixel width when moved between
|
// Docked side panels (Properties, block palette, future palettes) live
|
||||||
// edges; auto-collapse swaps the full panel for a hoverable rail.
|
// in an ordered vertical stack on the left/right edge of the drawing
|
||||||
let show_properties = !tab.is_start && self.show_properties && !self.clean_screen;
|
// view. Auto-collapsing (pinned) panels that aren't being hovered
|
||||||
let properties_width = self
|
// reduce to a rail sharing 1/N of the edge column's height; the
|
||||||
.properties_width
|
// hovered or unpinned panel expands to the full column height on top.
|
||||||
.min((self.win_size.0 * 0.45).clamp(220.0, 600.0));
|
let visible_panel = |id: crate::ui::dock::PanelId| -> bool {
|
||||||
let properties_el: Option<Element<'_, Message>> = show_properties.then(|| {
|
if tab.is_start || self.clean_screen {
|
||||||
let narrow_collapsed = self.win_size.0 < 1000.0
|
return false;
|
||||||
&& !self.props_expanded
|
|
||||||
&& !self.properties_hovered;
|
|
||||||
let auto_collapsed = self.properties_auto_collapse
|
|
||||||
&& !self.properties_hovered
|
|
||||||
&& !self.properties_dragging
|
|
||||||
&& !self.properties_resizing;
|
|
||||||
if narrow_collapsed || auto_collapsed {
|
|
||||||
collapse_bar(
|
|
||||||
"Properties",
|
|
||||||
self.properties_side,
|
|
||||||
Message::TogglePropertiesBar,
|
|
||||||
Message::PropertiesHover(true),
|
|
||||||
26.0,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let panel = tab
|
|
||||||
.properties
|
|
||||||
.view(properties_width, self.properties_auto_collapse);
|
|
||||||
let divider = properties_divider();
|
|
||||||
let group: Element<'_, Message> = match self.properties_side {
|
|
||||||
crate::app::config::DockSide::Left => row![panel, divider].into(),
|
|
||||||
crate::app::config::DockSide::Right => row![divider, panel].into(),
|
|
||||||
};
|
|
||||||
mouse_area(group)
|
|
||||||
.on_enter(Message::PropertiesHover(true))
|
|
||||||
.on_exit(Message::PropertiesHover(false))
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
});
|
match id {
|
||||||
|
crate::ui::dock::PanelId::Properties => self.show_properties,
|
||||||
// The Block Palette docks through the general edge-stack dock (side,
|
crate::ui::dock::PanelId::BlockPalette => self.show_block_palette,
|
||||||
// width, auto-collapse are persisted on `self.dock`). When auto-collapsing
|
|
||||||
// and not hovered it reduces to a rail; otherwise it renders full-height
|
|
||||||
// with themed pin/close chrome and a grabbable, resizable header.
|
|
||||||
let block_palette_el: Option<Element<'_, Message>> = if !tab.is_start
|
|
||||||
&& self.show_block_palette
|
|
||||||
&& !self.clean_screen
|
|
||||||
{
|
|
||||||
let id = crate::ui::dock::PanelId::BlockPalette;
|
|
||||||
let side = self
|
|
||||||
.dock
|
|
||||||
.location(id)
|
|
||||||
.map(|(s, _)| s)
|
|
||||||
.unwrap_or(crate::app::config::DockSide::Right);
|
|
||||||
let width = self.dock.width(id, self.win_size.0);
|
|
||||||
let auto_collapse = self.dock.auto_collapse(id);
|
|
||||||
let expanded = self.dock_expanded == Some(id);
|
|
||||||
if auto_collapse && !expanded {
|
|
||||||
Some(collapse_bar(
|
|
||||||
"Block Palette",
|
|
||||||
side,
|
|
||||||
Message::Dock(crate::ui::dock::DockMsg::Hover(id)),
|
|
||||||
Message::Dock(crate::ui::dock::DockMsg::Hover(id)),
|
|
||||||
26.0,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
let panel =
|
|
||||||
crate::ui::window::block_palette::view(&self.block_palette, width, auto_collapse);
|
|
||||||
let divider = dock_divider(id);
|
|
||||||
let group: Element<'_, Message> = match side {
|
|
||||||
crate::app::config::DockSide::Left => row![panel, divider].into(),
|
|
||||||
crate::app::config::DockSide::Right => row![divider, panel].into(),
|
|
||||||
};
|
|
||||||
Some(
|
|
||||||
mouse_area(group)
|
|
||||||
.on_enter(Message::Dock(crate::ui::dock::DockMsg::Hover(id)))
|
|
||||||
.on_exit(Message::Dock(crate::ui::dock::DockMsg::HoverExit))
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
let bp_side = self
|
let edge_stack =
|
||||||
.dock
|
|side: crate::app::config::DockSide| -> Option<Element<'_, Message>> {
|
||||||
.location(crate::ui::dock::PanelId::BlockPalette)
|
let ids: Vec<crate::ui::dock::PanelId> = match side {
|
||||||
.map(|(s, _)| s)
|
crate::app::config::DockSide::Left => self.dock.left.clone(),
|
||||||
.unwrap_or(crate::app::config::DockSide::Right);
|
crate::app::config::DockSide::Right => self.dock.right.clone(),
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.filter(|id| visible_panel(*id))
|
||||||
|
.collect();
|
||||||
|
if ids.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(self.build_edge_stack(side, &ids, tab))
|
||||||
|
};
|
||||||
|
let left_edge = edge_stack(crate::app::config::DockSide::Left);
|
||||||
|
let right_edge = edge_stack(crate::app::config::DockSide::Right);
|
||||||
|
|
||||||
// Command-line sits as a bottom-centre overlay on top of the
|
// Command-line sits as a bottom-centre overlay on top of the
|
||||||
// viewport stack rather than as a separate row in the main
|
// viewport stack rather than as a separate row in the main
|
||||||
|
|
@ -1562,52 +1506,93 @@ impl OpenCADStudio {
|
||||||
&& !tab.dyn_fields.is_empty())
|
&& !tab.dyn_fields.is_empty())
|
||||||
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
||||||
|| self.text_inline.is_some();
|
|| self.text_inline.is_some();
|
||||||
let properties_side = self.properties_side;
|
// The workspace row is: left edge stack, viewport, right edge stack.
|
||||||
// Combine the Properties panel and Block Palette into one row with the
|
let mut parts: Vec<Element<'_, Message>> = Vec::new();
|
||||||
// viewport, each placed on its own dock side. Ordering is stable so two
|
if let Some(e) = left_edge {
|
||||||
// panels sharing an edge never swap unpredictably.
|
parts.push(e);
|
||||||
let mut left_items: Vec<Element<'_, Message>> = Vec::new();
|
|
||||||
let mut right_items: Vec<Element<'_, Message>> = Vec::new();
|
|
||||||
if let Some(p) = properties_el {
|
|
||||||
match properties_side {
|
|
||||||
crate::app::config::DockSide::Left => left_items.push(p),
|
|
||||||
crate::app::config::DockSide::Right => right_items.push(p),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(p) = block_palette_el {
|
|
||||||
match bp_side {
|
|
||||||
crate::app::config::DockSide::Left => left_items.push(p),
|
|
||||||
crate::app::config::DockSide::Right => right_items.push(p),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut parts = left_items;
|
|
||||||
parts.push(viewport_stack.into());
|
parts.push(viewport_stack.into());
|
||||||
parts.extend(right_items);
|
if let Some(e) = right_edge {
|
||||||
|
parts.push(e);
|
||||||
|
}
|
||||||
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
||||||
|
|
||||||
let any_dragging = self.properties_dragging || self.dock_dragging.is_some();
|
let any_dragging = self.dock_dragging.is_some();
|
||||||
let any_resizing = self.properties_resizing || self.dock_resizing.is_some();
|
let any_resizing = self.dock_resizing.is_some();
|
||||||
let workspace = if any_dragging {
|
let workspace = if any_dragging {
|
||||||
let (preview_side, preview_width) = if let Some(id) = self.dock_dragging {
|
let id = self.dock_dragging.expect("guarded by any_dragging");
|
||||||
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
|
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
|
||||||
self.dock
|
self.dock
|
||||||
.location(id)
|
.location(id)
|
||||||
.map(|(s, _)| s)
|
.map(|(s, _)| s)
|
||||||
.unwrap_or(crate::app::config::DockSide::Right),
|
.unwrap_or(crate::app::config::DockSide::Right),
|
||||||
);
|
);
|
||||||
(side, self.dock.width(id, self.win_size.0))
|
// Dropping onto an edge joins that edge's stack, whose column is as
|
||||||
} else {
|
// wide as its widest panel (the dragged panel included).
|
||||||
let side = self.properties_dock_preview.unwrap_or(self.properties_side);
|
let preview_width = {
|
||||||
(side, properties_width)
|
let mut widths: Vec<f32> = match side {
|
||||||
|
crate::app::config::DockSide::Left => self.dock.left.clone(),
|
||||||
|
crate::app::config::DockSide::Right => self.dock.right.clone(),
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.map(|pid| self.dock.width(pid, self.win_size.0))
|
||||||
|
.collect();
|
||||||
|
widths.push(self.dock.width(id, self.win_size.0));
|
||||||
|
widths.into_iter().fold(0.0, f32::max)
|
||||||
};
|
};
|
||||||
let preview = container(Space::new())
|
// Faint tint over the whole edge column so the target edge reads
|
||||||
|
// even before the panel-sized ghost snaps to a slot.
|
||||||
|
let edge_tint = container(Space::new())
|
||||||
.width(Length::Fixed(preview_width))
|
.width(Length::Fixed(preview_width))
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.style(|theme: &Theme| {
|
.style(|theme: &Theme| {
|
||||||
let palette = theme.palette();
|
let palette = theme.palette();
|
||||||
container::Style {
|
container::Style {
|
||||||
background: Some(Background::Color(
|
background: Some(Background::Color(
|
||||||
palette.primary.weak.color.scale_alpha(0.72),
|
palette.primary.weak.color.scale_alpha(0.35),
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// The ghost is the dragged panel at its real size: its own saved
|
||||||
|
// width and its 1/N share of the edge (N = stack size after the
|
||||||
|
// drop), with a header naming which panel is being moved.
|
||||||
|
let was_here = self.dock.location(id).map(|(s, _)| s) == Some(side);
|
||||||
|
let final_count = if was_here {
|
||||||
|
std::cmp::max(self.dock.len(side), 1)
|
||||||
|
} else {
|
||||||
|
self.dock.len(side) + 1
|
||||||
|
};
|
||||||
|
let slot_h = self.win_size.1 / final_count as f32;
|
||||||
|
let index = self.dock_drag_target.map(|(_, i)| i).unwrap_or(0);
|
||||||
|
let slot = index.min(final_count.saturating_sub(1));
|
||||||
|
let ghost_top = slot as f32 * slot_h;
|
||||||
|
let ghost_header = container(text(id.title()).size(12))
|
||||||
|
.width(Fill)
|
||||||
|
.padding(iced::Padding {
|
||||||
|
top: 5.0,
|
||||||
|
right: 8.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 8.0,
|
||||||
|
})
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let ghost_panel = container(column![ghost_header, Space::new()])
|
||||||
|
.width(Length::Fixed(self.dock.width(id, self.win_size.0)))
|
||||||
|
.height(Length::Fixed(slot_h))
|
||||||
|
.style(|theme: &Theme| {
|
||||||
|
let palette = theme.palette();
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
palette.background.base.color,
|
||||||
)),
|
)),
|
||||||
border: Border {
|
border: Border {
|
||||||
color: palette.primary.base.color,
|
color: palette.primary.base.color,
|
||||||
|
|
@ -1617,39 +1602,74 @@ impl OpenCADStudio {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Position the ghost at its drop slot, flush against the edge.
|
||||||
|
let ghost = container(ghost_panel)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.align_x(match side {
|
||||||
|
crate::app::config::DockSide::Left => {
|
||||||
|
iced::alignment::Horizontal::Left
|
||||||
|
}
|
||||||
|
crate::app::config::DockSide::Right => {
|
||||||
|
iced::alignment::Horizontal::Right
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.align_y(iced::alignment::Vertical::Top)
|
||||||
|
.padding(iced::Padding {
|
||||||
|
top: ghost_top,
|
||||||
|
right: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
left: 0.0,
|
||||||
|
});
|
||||||
|
// Thin line across the column at the drop slot's top boundary.
|
||||||
|
let drop_line = container(
|
||||||
|
container(Space::new())
|
||||||
|
.width(Fill)
|
||||||
|
.height(Length::Fixed(3.0))
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme.palette().primary.base.color,
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.align_y(iced::alignment::Vertical::Top)
|
||||||
|
.padding(iced::Padding {
|
||||||
|
top: ghost_top,
|
||||||
|
right: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
left: 0.0,
|
||||||
|
});
|
||||||
|
let preview = iced::widget::stack![edge_tint, ghost, drop_line]
|
||||||
|
.width(Length::Fixed(preview_width))
|
||||||
|
.height(Fill);
|
||||||
let preview = container(preview)
|
let preview = container(preview)
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.align_x(match preview_side {
|
.align_x(match side {
|
||||||
crate::app::config::DockSide::Left => iced::alignment::Horizontal::Left,
|
crate::app::config::DockSide::Left => {
|
||||||
crate::app::config::DockSide::Right => iced::alignment::Horizontal::Right,
|
iced::alignment::Horizontal::Left
|
||||||
|
}
|
||||||
|
crate::app::config::DockSide::Right => {
|
||||||
|
iced::alignment::Horizontal::Right
|
||||||
|
}
|
||||||
});
|
});
|
||||||
stack![workspace, preview].width(Fill).height(Fill).into()
|
stack![workspace, preview].width(Fill).height(Fill).into()
|
||||||
} else {
|
} else {
|
||||||
workspace
|
workspace
|
||||||
};
|
};
|
||||||
let workspace: Element<'_, Message> = if any_dragging || any_resizing {
|
let workspace: Element<'_, Message> = if any_dragging || any_resizing {
|
||||||
if self.properties_dragging || self.properties_resizing {
|
mouse_area(workspace)
|
||||||
mouse_area(workspace)
|
.on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p)))
|
||||||
.on_move(Message::PropertiesDragMove)
|
.on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease))
|
||||||
.on_release(Message::PropertiesDragRelease)
|
.interaction(if any_resizing {
|
||||||
.interaction(if self.properties_resizing {
|
iced::mouse::Interaction::ResizingHorizontally
|
||||||
iced::mouse::Interaction::ResizingHorizontally
|
} else {
|
||||||
} else {
|
iced::mouse::Interaction::Grabbing
|
||||||
iced::mouse::Interaction::Grabbing
|
})
|
||||||
})
|
.into()
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
mouse_area(workspace)
|
|
||||||
.on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p)))
|
|
||||||
.on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease))
|
|
||||||
.interaction(if self.dock_resizing.is_some() {
|
|
||||||
iced::mouse::Interaction::ResizingHorizontally
|
|
||||||
} else {
|
|
||||||
iced::mouse::Interaction::Grabbing
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
workspace
|
workspace
|
||||||
};
|
};
|
||||||
|
|
@ -2231,6 +2251,148 @@ impl OpenCADStudio {
|
||||||
iced::advanced::widget::operation::focusable::unfocus(),
|
iced::advanced::widget::operation::focusable::unfocus(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build one edge: a narrow tab strip at the very edge plus the expanded
|
||||||
|
/// panels sliding out beside it. Auto-collapsing (pinned) panels are always
|
||||||
|
/// visible as equal-height 1/N tabs in the strip (via `Fill` distribution);
|
||||||
|
/// hovering a tab raises its panel to full column height beside the strip,
|
||||||
|
/// so switching between stacked panels is a direct tab-to-tab hover with no
|
||||||
|
/// width-jumping collapse dance. Unpinned panels are always expanded. The
|
||||||
|
/// whole edge is one hover region: leaving it collapses any auto-collapsing
|
||||||
|
/// panel, but moving between the tabs and the expanded panel does not.
|
||||||
|
fn build_edge_stack<'a>(
|
||||||
|
&'a self,
|
||||||
|
side: crate::app::config::DockSide,
|
||||||
|
ids: &[crate::ui::dock::PanelId],
|
||||||
|
tab: &'a DocumentTab,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let pinned: Vec<crate::ui::dock::PanelId> = ids
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| self.dock.auto_collapse(*id))
|
||||||
|
.collect();
|
||||||
|
// Expanded = unpinned panels (always open) in stack order, then the
|
||||||
|
// hovered auto-collapsing one last so it floats on top.
|
||||||
|
let mut expanded: Vec<crate::ui::dock::PanelId> = ids
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| !self.dock.auto_collapse(*id))
|
||||||
|
.collect();
|
||||||
|
if let Some(id) = self.dock_expanded {
|
||||||
|
if ids.contains(&id) && self.dock.auto_collapse(id) {
|
||||||
|
expanded.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Each expanded panel renders at its own saved width; the column is as
|
||||||
|
// wide as the widest one currently showing.
|
||||||
|
let col_w = expanded
|
||||||
|
.iter()
|
||||||
|
.map(|id| self.dock.width(*id, self.win_size.0))
|
||||||
|
.fold(0.0, f32::max);
|
||||||
|
|
||||||
|
let tab_strip = (!pinned.is_empty()).then(|| {
|
||||||
|
let tabs: Vec<Element<'_, Message>> = pinned
|
||||||
|
.iter()
|
||||||
|
.map(|id| self.rail_slice(*id, side))
|
||||||
|
.collect();
|
||||||
|
column(tabs).width(DOCK_RAIL_W).height(Fill).into()
|
||||||
|
});
|
||||||
|
let expanded_stack = (!expanded.is_empty()).then(|| {
|
||||||
|
// Multiple simultaneously-expanded panels (e.g. two unpinned panels
|
||||||
|
// on the same edge) stack one below the other, each taking 1/N of
|
||||||
|
// the column height via equal `Fill` shares — never overlapping.
|
||||||
|
let layers: Vec<Element<'_, Message>> = expanded
|
||||||
|
.iter()
|
||||||
|
.map(|id| self.expanded_panel(*id, side, col_w, tab))
|
||||||
|
.collect();
|
||||||
|
column(layers).height(Fill).into()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut children: Vec<Element<'_, Message>> = Vec::new();
|
||||||
|
match side {
|
||||||
|
crate::app::config::DockSide::Left => {
|
||||||
|
if let Some(t) = tab_strip {
|
||||||
|
children.push(t);
|
||||||
|
}
|
||||||
|
if let Some(e) = expanded_stack {
|
||||||
|
children.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::app::config::DockSide::Right => {
|
||||||
|
if let Some(e) = expanded_stack {
|
||||||
|
children.push(e);
|
||||||
|
}
|
||||||
|
if let Some(t) = tab_strip {
|
||||||
|
children.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mouse_area(row(children).height(Fill))
|
||||||
|
.on_exit(Message::Dock(crate::ui::dock::DockMsg::HoverExit))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One narrow tab in the edge strip. Equal `Fill` heights across the tabs
|
||||||
|
/// give each 1/N of the strip; hovering or clicking it raises its panel.
|
||||||
|
fn rail_slice(
|
||||||
|
&self,
|
||||||
|
id: crate::ui::dock::PanelId,
|
||||||
|
side: crate::app::config::DockSide,
|
||||||
|
) -> Element<'_, Message> {
|
||||||
|
let label = canvas(VBarLabel {
|
||||||
|
text: id.title().to_string(),
|
||||||
|
clockwise: side == crate::app::config::DockSide::Left,
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill);
|
||||||
|
mouse_area(
|
||||||
|
container(label)
|
||||||
|
.width(Length::Fixed(DOCK_RAIL_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()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.interaction(iced::mouse::Interaction::Pointer)
|
||||||
|
.on_press(Message::Dock(crate::ui::dock::DockMsg::DockGrab(id)))
|
||||||
|
.on_enter(Message::Dock(crate::ui::dock::DockMsg::Hover(id)))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A panel expanded to the full edge column: the panel body plus a
|
||||||
|
/// grabbable divider against the viewport. 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 auto_collapse = self.dock.auto_collapse(id);
|
||||||
|
let panel: Element<'_, Message> = match id {
|
||||||
|
crate::ui::dock::PanelId::Properties => {
|
||||||
|
tab.properties.view(width, auto_collapse)
|
||||||
|
}
|
||||||
|
crate::ui::dock::PanelId::BlockPalette => {
|
||||||
|
crate::ui::window::block_palette::view(&self.block_palette, width, auto_collapse)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Document tab bar ───────────────────────────────────────────────────────
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
||||||
|
|
@ -2591,66 +2753,11 @@ impl canvas::Program<Message> for VBarLabel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collapsed panel rendered as a tall narrow bar with its name written along
|
/// Width of a collapsed (auto-collapsing) panel's tab in the edge strip.
|
||||||
/// it. It can be clicked on narrow windows or expanded by hover when auto-hide
|
const DOCK_RAIL_W: f32 = 28.0;
|
||||||
/// is enabled.
|
|
||||||
pub(super) fn collapse_bar<'a>(
|
|
||||||
name: &str,
|
|
||||||
side: crate::app::config::DockSide,
|
|
||||||
on_press: Message,
|
|
||||||
on_enter: Message,
|
|
||||||
width: f32,
|
|
||||||
) -> Element<'a, Message> {
|
|
||||||
let label = canvas(VBarLabel {
|
|
||||||
text: name.to_string(),
|
|
||||||
clockwise: side == crate::app::config::DockSide::Left,
|
|
||||||
})
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill);
|
|
||||||
|
|
||||||
mouse_area(
|
|
||||||
container(label)
|
|
||||||
.width(iced::Length::Fixed(width))
|
|
||||||
.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()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.interaction(iced::mouse::Interaction::Pointer)
|
|
||||||
.on_press(on_press)
|
|
||||||
.on_enter(on_enter)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Grabbable separator between the docked panel and drawing view. The visible
|
|
||||||
/// line is wider than a single pixel so it remains discoverable in every theme.
|
|
||||||
fn properties_divider() -> Element<'static, Message> {
|
|
||||||
let line = container(Space::new())
|
|
||||||
.width(Length::Fixed(5.0))
|
|
||||||
.height(Fill)
|
|
||||||
.style(|theme: &Theme| container::Style {
|
|
||||||
background: Some(Background::Color(
|
|
||||||
theme.palette().background.neutral.color,
|
|
||||||
)),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
mouse_area(line)
|
|
||||||
.on_press(Message::PropertiesResizeGrab)
|
|
||||||
.on_double_click(Message::PropertiesWidthReset)
|
|
||||||
.interaction(iced::mouse::Interaction::ResizingHorizontally)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Grabbable separator for a docked panel managed by the general dock. Same
|
/// Grabbable separator for a docked panel managed by the general dock. Same
|
||||||
/// visual as `properties_divider` but emits generic dock messages for `id`.
|
/// visual as the previous per-panel divider but emits generic dock messages.
|
||||||
fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
|
fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
|
||||||
let line = container(Space::new())
|
let line = container(Space::new())
|
||||||
.width(Length::Fixed(5.0))
|
.width(Length::Fixed(5.0))
|
||||||
|
|
|
||||||
|
|
@ -196,15 +196,6 @@ impl DockState {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when the panel is in a non-empty reduced state on `side`, i.e. the
|
|
||||||
/// number of reduced panels that share the side's column height.
|
|
||||||
pub fn reduced_count(&self, side: DockSide, expanded: Option<PanelId>) -> usize {
|
|
||||||
self.stack(side)
|
|
||||||
.iter()
|
|
||||||
.filter(|id| Some(**id) != expanded)
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Number of panels currently docked on `side`.
|
/// Number of panels currently docked on `side`.
|
||||||
pub fn len(&self, side: DockSide) -> usize {
|
pub fn len(&self, side: DockSide) -> usize {
|
||||||
self.stack(side).len()
|
self.stack(side).len()
|
||||||
|
|
@ -228,49 +219,10 @@ pub const DOCK_MIN_W: f32 = 200.0;
|
||||||
/// Largest docked width a panel may be dragged or sized to.
|
/// Largest docked width a panel may be dragged or sized to.
|
||||||
pub const DOCK_MAX_W: f32 = 600.0;
|
pub const DOCK_MAX_W: f32 = 600.0;
|
||||||
|
|
||||||
/// Height allocated to one reduced panel sharing an edge with `count` others.
|
|
||||||
pub fn slice_height(count: usize, avail: f32) -> f32 {
|
|
||||||
if count == 0 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
avail / count as f32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Index (top → bottom) of the reduced slice whose bounds contain screen-local
|
|
||||||
/// `y` measured from the top of the edge column. Returns `None` for a y outside
|
|
||||||
/// the column or when there are no reduced panels. Even when an expanded panel
|
|
||||||
/// overlays the column, this computed index lets a reduced sibling be raised.
|
|
||||||
pub fn slice_index_at_y(y: f32, count: usize, avail: f32) -> Option<usize> {
|
|
||||||
if count == 0 || avail <= 0.0 || y < 0.0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let h = avail / count as f32;
|
|
||||||
let i = (y / h) as usize;
|
|
||||||
(i < count).then_some(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn slice_height_divides_equally() {
|
|
||||||
assert_eq!(slice_height(3, 300.0), 100.0);
|
|
||||||
assert_eq!(slice_height(1, 300.0), 300.0);
|
|
||||||
assert_eq!(slice_height(0, 300.0), 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn slice_index_maps_y_to_panel() {
|
|
||||||
assert_eq!(slice_index_at_y(50.0, 3, 300.0), Some(0));
|
|
||||||
assert_eq!(slice_index_at_y(110.0, 3, 300.0), Some(1));
|
|
||||||
assert_eq!(slice_index_at_y(299.0, 3, 300.0), Some(2));
|
|
||||||
assert_eq!(slice_index_at_y(300.0, 3, 300.0), None);
|
|
||||||
assert_eq!(slice_index_at_y(-1.0, 3, 300.0), None);
|
|
||||||
assert_eq!(slice_index_at_y(50.0, 0, 300.0), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_docks_each_known_panel_on_an_edge() {
|
fn default_docks_each_known_panel_on_an_edge() {
|
||||||
let state = DockState::default();
|
let state = DockState::default();
|
||||||
|
|
@ -336,16 +288,6 @@ mod tests {
|
||||||
assert_eq!(state.width(PanelId::BlockPalette, 800.0), DOCK_MAX_W.min(360.0));
|
assert_eq!(state.width(PanelId::BlockPalette, 800.0), DOCK_MAX_W.min(360.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reduced_count_excludes_expanded() {
|
|
||||||
let state = DockState::default(); // right: [BlockPalette]
|
|
||||||
assert_eq!(state.reduced_count(DockSide::Right, None), 1);
|
|
||||||
assert_eq!(
|
|
||||||
state.reduced_count(DockSide::Right, Some(PanelId::BlockPalette)),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn drop_index_maps_between_zero_and_total() {
|
fn drop_index_maps_between_zero_and_total() {
|
||||||
assert_eq!(drop_index(0.0, 3, 300.0), 0);
|
assert_eq!(drop_index(0.0, 3, 300.0), 0);
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,7 @@ impl PropertiesPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
pub fn view(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
||||||
|
use crate::ui::dock::{DockMsg, PanelId};
|
||||||
// ── Header ──────────────────────────────────────────────────────────
|
// ── Header ──────────────────────────────────────────────────────────
|
||||||
let pin_icon = if auto_collapse {
|
let pin_icon = if auto_collapse {
|
||||||
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
|
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
|
||||||
|
|
@ -376,7 +377,7 @@ impl PropertiesPanel {
|
||||||
crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
|
crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
|
||||||
};
|
};
|
||||||
let pin = button(pin_icon)
|
let pin = button(pin_icon)
|
||||||
.on_press(Message::PropertiesAutoCollapseToggle)
|
.on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Properties)))
|
||||||
.style(move |theme: &Theme, status| {
|
.style(move |theme: &Theme, status| {
|
||||||
let mut style = button::subtle(theme, status);
|
let mut style = button::subtle(theme, status);
|
||||||
if auto_collapse {
|
if auto_collapse {
|
||||||
|
|
@ -395,7 +396,7 @@ impl PropertiesPanel {
|
||||||
crate::ui::icons::CLOSE,
|
crate::ui::icons::CLOSE,
|
||||||
12.0,
|
12.0,
|
||||||
))
|
))
|
||||||
.on_press(Message::PropertiesClose)
|
.on_press(Message::Dock(DockMsg::Close(PanelId::Properties)))
|
||||||
.style(button::subtle)
|
.style(button::subtle)
|
||||||
.padding([3, 5]);
|
.padding([3, 5]);
|
||||||
let close = tooltip(
|
let close = tooltip(
|
||||||
|
|
@ -425,7 +426,7 @@ impl PropertiesPanel {
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.padding([3, 6]),
|
.padding([3, 6]),
|
||||||
)
|
)
|
||||||
.on_press(Message::PropertiesDockGrab)
|
.on_press(Message::Dock(DockMsg::DockGrab(PanelId::Properties)))
|
||||||
.interaction(iced::mouse::Interaction::Grab);
|
.interaction(iced::mouse::Interaction::Grab);
|
||||||
|
|
||||||
// ── Title bar (entity type / "No Selection") ─────────────────────
|
// ── Title bar (entity type / "No Selection") ─────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue