forked from 1b-agent/opencadstudio-upstream
Merge main into Select-Color-Redesign
Resolve conflicts with the new gradient and graphic-attributes colour targets: keep main's targets and menu handling, routed through the new ColorPicker state; adapt dock/view.rs to `color_picker`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
220 files changed
+37313
-5826
No files matched your search
+38
-440
@@ -9,11 +9,11 @@ use crate::t;
|
||||
use crate::ui::wrap_bar::DensitySwap;
|
||||
use crate::ui::wrap_bar::WrapFlow;
|
||||
use iced::widget::{
|
||||
button, canvas, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader,
|
||||
stack, text, Row, Space,
|
||||
button, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader, stack,
|
||||
text, Row, Space,
|
||||
};
|
||||
use iced::window;
|
||||
use iced::{keyboard, Background, Border, Color, Element, Fill, Length, Subscription, Task, Theme};
|
||||
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
||||
use iced_aw::ContextMenu;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1889,29 +1889,10 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
viewport_stack = viewport_stack.push(tab.graph.view(sections));
|
||||
}
|
||||
|
||||
// Docked side panels (Properties, block palette, future palettes) live
|
||||
// in an ordered vertical stack on the left/right edge of the drawing
|
||||
// view. Auto-collapsing (pinned) panels that aren't being hovered
|
||||
// reduce to a rail sharing 1/N of the edge column's height; the
|
||||
// hovered or unpinned panel expands to the full column height on top.
|
||||
// Single source of truth with dock_visible_len (#57): one new panel
|
||||
// updated in only one place used to break drag math vs rendering.
|
||||
let visible_panel = |id: crate::ui::dock::PanelId| -> bool { self.dock_panel_visible(id) };
|
||||
let edge_stack = |side: crate::app::config::DockSide| -> Option<Element<'_, Message>> {
|
||||
let ids: Vec<crate::ui::dock::PanelId> = match side {
|
||||
crate::app::config::DockSide::Left => self.dock.left.clone(),
|
||||
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);
|
||||
// Docked pallets: each edge's icon strip and shown group (see
|
||||
// `app::dock`).
|
||||
let left_edge = self.build_edge_stack(crate::app::config::DockSide::Left, tab);
|
||||
let right_edge = self.build_edge_stack(crate::app::config::DockSide::Right, tab);
|
||||
|
||||
// Command-line sits as a bottom-centre overlay on top of the
|
||||
// viewport stack rather than as a separate row in the main
|
||||
@@ -1957,158 +1938,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
}
|
||||
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
||||
|
||||
let any_dragging = self.dock_dragging.is_some();
|
||||
let any_resizing =
|
||||
self.dock_resizing.is_some() || self.xref_col_drag.is_some() || self.xref_split_drag;
|
||||
let workspace = if any_dragging {
|
||||
let id = self.dock_dragging.expect("guarded by any_dragging");
|
||||
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
|
||||
self.dock
|
||||
.location(id)
|
||||
.map(|(s, _)| s)
|
||||
.unwrap_or(crate::app::config::DockSide::Right),
|
||||
);
|
||||
// Dropping onto an edge joins that edge's stack, whose column is as
|
||||
// wide as its widest currently-shown panel (the dragged panel
|
||||
// included). Hidden (closed) panels don't render, so they can't
|
||||
// widen the column being previewed.
|
||||
let preview_width = {
|
||||
let mut widths: Vec<f32> = match side {
|
||||
crate::app::config::DockSide::Left => &self.dock.left,
|
||||
crate::app::config::DockSide::Right => &self.dock.right,
|
||||
}
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&pid| self.dock_panel_visible(pid))
|
||||
.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)
|
||||
};
|
||||
// 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))
|
||||
.height(Fill)
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(
|
||||
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 = number of panels shown
|
||||
// on the edge after the drop), with a header naming which panel is
|
||||
// being moved. Only visibly-docked panels share the column height:
|
||||
// a hidden (closed) panel keeps its stack slot but no screen space,
|
||||
// so it must not split the preview.
|
||||
let was_here = self.dock.location(id).map(|(s, _)| s) == Some(side);
|
||||
let visible = self.dock_visible_len(side);
|
||||
let final_count = if was_here {
|
||||
std::cmp::max(visible, 1)
|
||||
} else {
|
||||
visible + 1
|
||||
};
|
||||
let edge_h = tab.scene.selection.borrow().view.vp_size.1;
|
||||
let slot_h = edge_h / 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 {
|
||||
color: palette.primary.base.color,
|
||||
width: 2.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..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)
|
||||
.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,
|
||||
});
|
||||
stack![workspace, preview].width(Fill).height(Fill).into()
|
||||
} else {
|
||||
workspace
|
||||
};
|
||||
let workspace: Element<'_, Message> = if any_dragging || any_resizing {
|
||||
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 any_resizing {
|
||||
iced::mouse::Interaction::ResizingHorizontally
|
||||
} else {
|
||||
iced::mouse::Interaction::Grabbing
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
workspace
|
||||
};
|
||||
let workspace = self.dock_overlay(workspace, tab);
|
||||
let command_line = self.command_line.view(
|
||||
allow_autocomplete,
|
||||
dyn_capturing,
|
||||
@@ -2581,6 +2411,18 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
Subscription::none()
|
||||
};
|
||||
// C11: park a `capture` request until the window draws another frame;
|
||||
// a timer fallback prevents a request from hanging when no redraw is
|
||||
// pending.
|
||||
let capture_frame = if self.control.capture_frame_pending {
|
||||
Subscription::batch([
|
||||
window::frames().map(|_| Message::ControlCaptureFrame { redrew: true }),
|
||||
iced::time::every(std::time::Duration::from_millis(50))
|
||||
.map(|_| Message::ControlCaptureFrame { redrew: false }),
|
||||
])
|
||||
} else {
|
||||
Subscription::none()
|
||||
};
|
||||
// Blink the MText preview caret while the editor is open.
|
||||
let caret_blink = if self.mtext_editor.is_some() {
|
||||
iced::time::every(std::time::Duration::from_millis(530))
|
||||
@@ -2596,9 +2438,9 @@ impl OpenCADStudio {
|
||||
iced::time::every(std::time::Duration::from_millis(300)).map(|_| Message::PollWebFonts);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let web_fonts = Subscription::none();
|
||||
// Periodic autosave to a `.sv$` recovery file (native only). SAVETIME is
|
||||
// the interval in minutes; 0 disables it.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
// Periodic autosave (SAVETIME): a `.sv$` recovery copy beside the file
|
||||
// on native, an OPFS snapshot on web. SAVETIME is the interval in
|
||||
// minutes; 0 disables it.
|
||||
let autosave = if self.savetime_min > 0 {
|
||||
iced::time::every(std::time::Duration::from_secs(
|
||||
self.savetime_min as u64 * 60,
|
||||
@@ -2607,8 +2449,6 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
Subscription::none()
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let autosave = Subscription::none();
|
||||
// Drawings handed over by a second launch (single instance). Inert in a
|
||||
// process that lost the port election, so it costs nothing there.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -2791,9 +2631,20 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
let shortcut = shortcut_key_name(&key, modifiers)?;
|
||||
(status == Status::Ignored
|
||||
|| crate::app::shortcuts::is_global_key(&shortcut))
|
||||
.then_some(Message::ShortcutPressed(shortcut))
|
||||
if status == Status::Ignored
|
||||
|| crate::app::shortcuts::is_global_key(&shortcut)
|
||||
{
|
||||
return Some(Message::ShortcutPressed(shortcut));
|
||||
}
|
||||
// A text field captured the key. SPACE rebound away from
|
||||
// COMMANDSPACE still fires from the command line, but
|
||||
// only there: other fields keep typing spaces. This
|
||||
// closure is a plain fn, so the binding is read from a
|
||||
// flag and the focus is asked for in update. (#1710)
|
||||
(shortcut == "SPACE"
|
||||
&& crate::app::shortcuts::SPACE_REBOUND_OFF_COMMANDSPACE
|
||||
.load(std::sync::atomic::Ordering::Relaxed))
|
||||
.then_some(Message::CommandLineSpaceProbe)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -2836,6 +2687,7 @@ impl OpenCADStudio {
|
||||
nav_settle,
|
||||
gpu_probe,
|
||||
thumbnail_capture,
|
||||
capture_frame,
|
||||
caret_blink,
|
||||
web_fonts,
|
||||
autosave,
|
||||
@@ -2854,198 +2706,6 @@ impl OpenCADStudio {
|
||||
pub(super) fn unfocus_widgets(&self) -> Task<Message> {
|
||||
iced::advanced::widget::operate(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,
|
||||
self.dock_expanded == Some(*id),
|
||||
self.dock_dragging == Some(*id),
|
||||
)
|
||||
})
|
||||
.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,
|
||||
is_active: bool,
|
||||
is_dragging: bool,
|
||||
) -> Element<'_, Message> {
|
||||
let label = canvas(VBarLabel {
|
||||
text: id.title().to_string(),
|
||||
clockwise: side == crate::app::config::DockSide::Left,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
let bg = move |theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
if is_active {
|
||||
palette.primary.weak.color
|
||||
} else if is_dragging {
|
||||
palette.primary.weak.color.scale_alpha(0.55)
|
||||
} else {
|
||||
palette.background.base.color
|
||||
}
|
||||
};
|
||||
let tab = container(label)
|
||||
.width(Length::Fixed(DOCK_RAIL_W))
|
||||
.height(Fill)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(bg(theme))),
|
||||
border: Border {
|
||||
color: if is_active {
|
||||
theme.palette().primary.base.color
|
||||
} else {
|
||||
theme.palette().background.neutral.color
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
mouse_area(tab)
|
||||
.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)
|
||||
}
|
||||
crate::ui::dock::PanelId::ExternalReferences => {
|
||||
self.xref_manager
|
||||
.view(width, auto_collapse, 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,
|
||||
auto_collapse,
|
||||
),
|
||||
crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse),
|
||||
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,
|
||||
auto_collapse,
|
||||
),
|
||||
crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view(
|
||||
&self.pc_manager,
|
||||
&tab.scene.document,
|
||||
&tab.scene.selected_handles_in_order(),
|
||||
width,
|
||||
auto_collapse,
|
||||
),
|
||||
crate::ui::dock::PanelId::SheetSetManager => {
|
||||
crate::ui::window::sheet_set::view(&self.sheet_set, 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 ───────────────────────────────────────────────────────
|
||||
@@ -3374,68 +3034,6 @@ fn pane_mouse_area<'a>(idx: usize) -> Element<'a, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Canvas that draws a label rotated 90° (for a collapsed panel's bar).
|
||||
struct VBarLabel {
|
||||
text: String,
|
||||
clockwise: bool,
|
||||
}
|
||||
|
||||
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(if self.clockwise {
|
||||
std::f32::consts::FRAC_PI_2
|
||||
} else {
|
||||
-std::f32::consts::FRAC_PI_2
|
||||
}));
|
||||
frame.fill_text(canvas::Text {
|
||||
content: self.text.clone(),
|
||||
position: iced::Point::ORIGIN,
|
||||
color: theme.palette().background.base.text.scale_alpha(0.72),
|
||||
size: iced::Pixels(13.0),
|
||||
align_x: iced::advanced::text::Alignment::Center,
|
||||
align_y: iced::alignment::Vertical::Center,
|
||||
shaping: iced::advanced::text::Shaping::Advanced,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of a collapsed (auto-collapsing) panel's tab in the edge strip.
|
||||
const DOCK_RAIL_W: f32 = 28.0;
|
||||
|
||||
/// Grabbable separator for a docked panel managed by the general dock. Same
|
||||
/// visual as the previous per-panel divider but emits generic dock messages.
|
||||
fn dock_divider(id: crate::ui::dock::PanelId) -> 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()
|
||||
});
|
||||
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()
|
||||
}
|
||||
|
||||
const START_ACTION_RADIUS: f32 = 6.0;
|
||||
|
||||
fn start_action_shape(mut style: button::Style) -> button::Style {
|
||||
|
||||
+86
-7
@@ -19,7 +19,6 @@ impl OpenCADStudio {
|
||||
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
|
||||
Some(K::UpdateNotice) => crate::tr!("modal", "update-available"),
|
||||
Some(K::DonationPrompt) => crate::tr!("donation", "title"),
|
||||
Some(K::Layers) => crate::tr!("modal", "layer-manager"),
|
||||
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
|
||||
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||
@@ -78,6 +77,8 @@ impl OpenCADStudio {
|
||||
Some(K::Recovery) => crate::tr!("modal", "recovery-report"),
|
||||
Some(K::MissingFonts) => crate::t!("Missing fonts").into_owned(),
|
||||
Some(K::RecoveryPrompt) => crate::tr!("modal", "recovery-prompt"),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Some(K::AutosaveRestore) => crate::t!("Recover Drawing").into_owned(),
|
||||
Some(K::GpuWarning) => crate::tr!("gpu", "title"),
|
||||
Some(K::XrefHelp) => crate::t!("Reference Manager Help").into_owned(),
|
||||
Some(K::SheetSet) => self
|
||||
@@ -464,12 +465,6 @@ impl OpenCADStudio {
|
||||
crate::ui::window::update_notice::view_window(latest, body, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::Layers => {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
sized_flow(ex, 900, 360, |flow| {
|
||||
tab.layers.view_window(self.layer_name_col_w, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::LayerTranslator => {
|
||||
use crate::modules::draw::layers::laytrans;
|
||||
let i = self.active_tab;
|
||||
@@ -1814,6 +1809,24 @@ impl OpenCADStudio {
|
||||
crate::ui::window::recovery::view_prompt(&opening.name, error, flow)
|
||||
})
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
super::super::ModalKind::AutosaveRestore => {
|
||||
let name = self
|
||||
.web_autosave_pending
|
||||
.first()
|
||||
.map(|entry| entry.name.clone())
|
||||
.unwrap_or_default();
|
||||
let pending = self.web_autosave_pending.len();
|
||||
// What the crashed session had open, when it remembered.
|
||||
let session_drawings = self
|
||||
.web_session
|
||||
.as_ref()
|
||||
.map(|session| session.tabs.len())
|
||||
.filter(|count| *count > 0);
|
||||
automatic_flow(ex, |flow| {
|
||||
autosave_restore_dialog_window(&name, pending, session_drawings, flow)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2061,6 +2074,72 @@ fn unsaved_changes_dialog_window(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Web crash-recovery prompt: an autosave snapshot of `name` is newer than
|
||||
/// the drawing's last save. Restore opens it as a recovered tab; Discard
|
||||
/// deletes the snapshot. With more than one snapshot still pending, "Restore
|
||||
/// all" puts the whole remaining list through the same guarded open queue.
|
||||
/// When the crashed session left a tab list behind, a muted line says how
|
||||
/// much it had open. Mirrors the unsaved-changes dialog's layout.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn autosave_restore_dialog_window(
|
||||
name: &str,
|
||||
pending: usize,
|
||||
session_drawings: Option<usize>,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'static, Message> {
|
||||
let mut body = column![text(crate::tf!(
|
||||
"\"{name}\" has an autosave copy newer than the last save."
|
||||
))
|
||||
.size(14)]
|
||||
.spacing(0);
|
||||
if let Some(count) = session_drawings {
|
||||
body = body
|
||||
.push(iced::widget::Space::new().height(4))
|
||||
.push(
|
||||
text(crate::tf!("Session had {count} open drawing(s).", count = count))
|
||||
.size(12)
|
||||
.style(dialog_muted_text_style),
|
||||
);
|
||||
}
|
||||
body = body
|
||||
.push(iced::widget::Space::new().height(4))
|
||||
.push(
|
||||
text(t!("Restore it as a recovered tab, or discard the copy."))
|
||||
.size(12)
|
||||
.style(dialog_muted_text_style),
|
||||
)
|
||||
.push(iced::widget::Space::new().height(20));
|
||||
let mut buttons = row![dialog_button(
|
||||
t!("Restore"),
|
||||
Message::WebAutosaveRestore,
|
||||
button::primary
|
||||
)];
|
||||
// Only worth its width once there is a batch to take in one click.
|
||||
if pending > 1 {
|
||||
buttons = buttons
|
||||
.push(iced::widget::Space::new().width(8))
|
||||
.push(dialog_button(
|
||||
t!("Restore all"),
|
||||
Message::WebAutosaveRestoreAll,
|
||||
button::secondary,
|
||||
));
|
||||
}
|
||||
buttons = buttons
|
||||
.push(iced::widget::Space::new().width(8))
|
||||
.push(dialog_button(
|
||||
t!("Discard"),
|
||||
Message::WebAutosaveDiscard,
|
||||
button::danger,
|
||||
));
|
||||
body = body.push(buttons);
|
||||
container(body)
|
||||
.style(dialog_body_style)
|
||||
.center_x(sizing.width)
|
||||
.center_y(sizing.height)
|
||||
.padding([24, 28])
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn file_in_use_dialog_window(
|
||||
path: &str,
|
||||
|
||||
+18
-9
@@ -1125,6 +1125,8 @@ const MENU_CURSOR_INSET_X: f32 = 24.0;
|
||||
/// not the row carries a glyph (object snaps, command icons).
|
||||
const MENU_GUTTER_W: f32 = 18.0;
|
||||
const MENU_ICON_SIZE: f32 = 14.0;
|
||||
/// Trailing check mark of checked rows; sits in the submenu caret column.
|
||||
const MENU_CHECK_SIZE: f32 = 10.0;
|
||||
|
||||
/// The gutter cell: the row's glyph, faded with a disabled row, or empty
|
||||
/// space of the same width.
|
||||
@@ -1264,16 +1266,13 @@ pub(super) fn viewport_context_menu_overlay(
|
||||
position_canvas_overlay_clamped(pos, bottom_inset, offset, panel.into())
|
||||
}
|
||||
|
||||
/// One menu row: label (bold for the default row, "✓ "-prefixed when
|
||||
/// checked), and the keyword hint right-aligned and dimmed. Disabled rows
|
||||
/// render without a press handler and with faded text.
|
||||
/// One menu row: label (bold for the default row), the keyword hint
|
||||
/// right-aligned and dimmed, and a check mark in the trailing column (where
|
||||
/// submenu headers draw their caret) when checked, so checked labels stay
|
||||
/// aligned with the others. Disabled rows render without a press handler and
|
||||
/// with faded text and icons.
|
||||
fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element<'static, Message> {
|
||||
let label = if item.checked {
|
||||
format!("✓ {}", item.label)
|
||||
} else {
|
||||
item.label.clone()
|
||||
};
|
||||
let mut label_text = text(label).size(12);
|
||||
let mut label_text = text(item.label.clone()).size(12);
|
||||
if item.default {
|
||||
label_text = label_text.font(iced::Font {
|
||||
weight: iced::font::Weight::Bold,
|
||||
@@ -1299,6 +1298,16 @@ fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element<
|
||||
}),
|
||||
);
|
||||
}
|
||||
if item.checked {
|
||||
if item.hint.is_none() {
|
||||
content = content.push(iced::widget::Space::new().width(Fill));
|
||||
}
|
||||
content = content.push(if enabled {
|
||||
crate::ui::icons::themed(crate::ui::icons::CHECK, MENU_CHECK_SIZE)
|
||||
} else {
|
||||
crate::ui::icons::themed_disabled(crate::ui::icons::CHECK, MENU_CHECK_SIZE)
|
||||
});
|
||||
}
|
||||
let mut btn = button(content)
|
||||
.padding(iced::Padding {
|
||||
top: 3.0,
|
||||
|
||||
Reference in new issue
Block a user