Merge remote-tracking branch 'origin/main' into plugin/host-model-api

# Conflicts:
#	crates/ocs_plugin_api/src/process/v4.rs
#	src/app/update/mod.rs
This commit is contained in:
Felix Riestra committed 2026-09-19 23:58:01 +02:00
commit 8648225ac5
309 files changed
+81166 -4157

No files matched your search

+1 -6
View File
@@ -1,14 +1,9 @@
use crate::app::Message;
use crate::t;
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, svg, text, Space};
use iced::{Background, Border, Element, Fill, Length, Shrink, Theme};
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn primary_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().primary.base.color),
+250 -21
View File
@@ -1,10 +1,13 @@
//! Command-alias editor — an in-canvas modal (Plan B) for adding, remapping and
//! removing command-line aliases (the `ocad.pgp` table). Opened by ALIASEDIT.
//! Rows are `(alias, command)`; edits are buffered in `alias_editor_rows` and
//! committed to the alias table when the dialog closes. Mirrors the editable-row
//! pattern of the attribute editor and plugin manager.
//! committed to the alias table on Apply. Mirrors the shortcut editor:
//! transient draft row with explicit accept, duplicate/unknown validation,
//! reset-to-defaults with confirmation, apply-and-exit, alias count, and an
//! unsaved-changes guard on close.
use crate::app::Message;
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Length, Theme};
use crate::t;
@@ -19,20 +22,54 @@ pub enum AliasField {
/// Right-hand lane reserved for the scrollbar so it never overlaps the ✕ column.
const GUTTER: f32 = 16.0;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
fn danger_text_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
color: Some(theme.palette().danger.base.color),
}
}
/// Text-input variant flagging an invalid value: danger border and value
/// color on the theme's danger weak background.
fn danger_input_style(
theme: &Theme,
status: iced::widget::text_input::Status,
) -> iced::widget::text_input::Style {
let danger = theme.palette().danger.base;
iced::widget::text_input::Style {
background: Background::Color(theme.palette().danger.weak.color),
border: iced::border::rounded(4)
.color(danger.color)
.width(
if matches!(
status,
iced::widget::text_input::Status::Focused { is_hovered: _ }
) {
1.5
} else {
1.0
},
),
icon: danger.color,
placeholder: danger.color.scale_alpha(0.7),
value: danger.text,
selection: danger.color,
}
}
/// Build the alias editor content. `rows` is the live working buffer.
pub fn view_window(
rows: &[(String, String)],
pub fn view_window<'a>(
rows: &'a [(String, String)],
pending_add: bool,
reset_confirm: bool,
duplicate_aliases: &rustc_hash::FxHashSet<String>,
duplicate_conflicts: &[(String, String)],
unknown_commands: &rustc_hash::FxHashSet<String>,
close_confirm: bool,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'_, Message> {
) -> Element<'a, Message> {
let title = text(t!("Command Aliases")).size(15);
let hint = text(t!(
"Type an alias and the command it runs (e.g. L → LINE). Apply to save to ocad.pgp; closing discards unapplied edits."
"Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row."
))
.size(11)
.style(muted_style);
@@ -46,7 +83,7 @@ pub fn view_window(
row![
container(text(t!("Alias")).size(11).style(muted_style)).width(Length::Fixed(120.0)),
container(text(t!("Command")).size(11).style(muted_style)).width(sizing.width),
Space::new().width(Length::Fixed(30.0)),
Space::new().width(Length::Fixed(62.0)),
]
.spacing(8),
)
@@ -54,39 +91,167 @@ pub fn view_window(
let mut list = column![].spacing(3);
for (idx, (alias, cmd)) in rows.iter().enumerate() {
let alias_key = alias.trim().to_uppercase();
let duplicate = !alias_key.is_empty() && duplicate_aliases.contains(alias_key.as_str());
let alias_box = text_input(t!("alias").as_ref(), alias)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
.size(13)
.padding([3, 6])
.width(Length::Fixed(120.0));
let alias_box = if duplicate {
alias_box.style(danger_input_style)
} else {
alias_box
};
let unknown_command =
!cmd.trim().is_empty() && unknown_commands.contains(cmd.trim());
let cmd_box = text_input(t!("command").as_ref(), cmd)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
.size(13)
.padding([3, 6])
.width(sizing.width);
let del = button(crate::ui::icons::themed_danger_text(crate::ui::icons::CLOSE, 12.0))
let cmd_box = if unknown_command {
cmd_box.style(danger_input_style)
} else {
cmd_box
};
// Draft rows get a check (finish the addition, without applying) and
// a cancel ✕; committed rows get the trash bin.
let remove = if pending_add && idx == 0 {
let done_ok = !alias.trim().is_empty()
&& !cmd.trim().is_empty()
&& !duplicate
&& !unknown_commands.contains(cmd.trim());
row![
button(crate::ui::icons::themed_success_text(
crate::ui::icons::CHECK,
12.0,
))
.on_press_maybe(done_ok.then_some(Message::AliasEditorDraftAccept))
.padding([2, 6])
.style(button::success),
button(crate::ui::icons::themed_danger_text(
crate::ui::icons::CLOSE,
12.0,
))
.on_press(Message::AliasEditorDraftCancel)
.padding([2, 6])
.style(button::danger),
]
.spacing(4)
.align_y(iced::Center)
} else {
row![button(crate::ui::icons::themed_danger_text(
crate::ui::icons::TRASH,
12.0,
))
.on_press(Message::AliasEditorRemove(idx))
.padding([2, 6])
.style(button::danger);
.style(button::danger)]
.align_y(iced::Center)
};
list = list.push(
row![alias_box, cmd_box, del]
row![alias_box, cmd_box, remove]
.spacing(8)
.align_y(iced::Center),
);
}
let add = button(text(t!("+ Add alias")).size(12))
.on_press(Message::AliasEditorAdd)
.padding([4, 10])
.style(button::secondary);
// While a draft row is pending, the add button doubles as the visible
// cancel affordance (Esc works too).
let add = if pending_add {
button(text(t!("Cancel add (Esc)")).size(12))
.on_press(Message::AliasEditorDraftCancel)
.padding([4, 10])
.style(button::danger)
} else {
button(text(t!("+ Add alias")).size(12))
.on_press(Message::AliasEditorAdd)
.padding([4, 10])
.style(button::secondary)
};
// The reset confirmation replaces the alias count so the warning
// gets the whole left side of the bar.
let stats = if reset_confirm {
row![Space::new().width(Length::Fixed(0.0))]
} else {
row![text(format!("{}: {}", t!("Number of aliases"), rows.len()))
.size(12)
.style(muted_style)]
};
// Reset asks for confirmation in place: the add/reset buttons are
// replaced by the question with Yes / No.
let (reset_area, add_area) = if reset_confirm {
(
row![
text(t!(
"Are you sure you want to reset? You will lose all your current aliases!"
))
.size(12)
.style(danger_text_style),
button(text(t!("Yes, reset")).size(12))
.on_press(Message::AliasEditorResetConfirm)
.padding([4, 10])
.style(button::danger),
button(text(t!("No")).size(12))
.on_press(Message::AliasEditorResetDeny)
.padding([4, 10])
.style(button::secondary),
]
.spacing(8)
.align_y(iced::Center),
row![Space::new().width(Length::Fixed(0.0))],
)
} else {
(
row![button(text(t!("Reset to default")).size(12))
.on_press(Message::AliasEditorResetAsk)
.padding([4, 10])
.style(button::secondary)]
.spacing(8)
.align_y(iced::Center),
row![add],
)
};
// Apply — primary action; commits the rows to ocad.pgp and stays open.
let apply = button(text(t!("Apply")).size(12))
.on_press(Message::AliasEditorApply)
.padding([4, 16])
.style(button::primary);
let apply_exit = button(text(t!("Apply && Exit")).size(12))
.on_press(Message::AliasEditorApplyExit)
.padding([4, 16])
.style(button::primary);
// The reset confirmation takes over the whole action bar.
let apply_area = if reset_confirm {
row![Space::new().width(Length::Fixed(0.0))]
} else {
row![apply, apply_exit].spacing(8).align_y(iced::Center)
};
container(
// Persistent validation warnings: shown until every conflicting alias is
// resolved (edited or a row removed) and every command is runnable.
let mut conflict_banner = column![].spacing(2);
for (alias, command) in duplicate_conflicts {
conflict_banner = conflict_banner.push(
text(crate::tf!(
"Alias already used for command: {} → {}",
alias,
command
))
.size(12)
.style(danger_text_style),
);
}
for command in unknown_commands {
conflict_banner = conflict_banner.push(
text(crate::tf!("Unknown command: {}", command))
.size(12)
.style(danger_text_style),
);
}
let content = container(
column![
title,
hint,
@@ -94,7 +259,11 @@ pub fn view_window(
head,
scrollable(container(list).padding(gutter)).height(sizing.height),
Space::new().height(6),
row![add, Space::new().width(sizing.width), apply].align_y(iced::Center),
conflict_banner,
Space::new().height(4),
row![stats, add_area, reset_area, Space::new().width(sizing.width), apply_area]
.spacing(8)
.align_y(iced::Center),
]
.spacing(6)
.width(sizing.width)
@@ -108,6 +277,66 @@ pub fn view_window(
theme.palette().background.base.color,
)),
..Default::default()
})
.into()
});
// Unsaved-changes guard: closing with un-applied rows stacks a dimmed
// shield and a confirmation panel on top of the editor.
if !close_confirm {
return content.into();
}
let shield = iced::widget::mouse_area(
container(Space::new())
.width(Length::Fill)
.height(Length::Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme
.palette()
.background
.strongest
.color
.scale_alpha(0.55),
)),
..Default::default()
}),
)
.on_press(Message::AliasEditorCloseKeep)
.interaction(iced::mouse::Interaction::Idle);
let panel = container(
column![
text(t!("Unsaved changes will be discarded.")).size(14),
Space::new().height(10),
row![
button(text(t!("Discard && close")).size(12))
.on_press(Message::AliasEditorCloseDiscard)
.padding([4, 12])
.style(button::danger),
button(text(t!("Keep editing")).size(12))
.on_press(Message::AliasEditorCloseKeep)
.padding([4, 12])
.style(button::secondary),
]
.spacing(8),
]
.spacing(4),
)
.padding(16)
.width(Length::Fixed(320.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
border: iced::Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
});
let centered = container(panel)
.width(Length::Fill)
.height(Length::Fill)
.align_x(iced::alignment::Horizontal::Center)
.align_y(iced::alignment::Vertical::Center);
iced::widget::stack![content, iced::widget::opaque(shield), centered].into()
}
+2 -20
View File
@@ -25,6 +25,8 @@ use std::borrow::Cow;
use std::fmt;
use crate::ui::properties::{lw_options, LwItem};
use crate::ui::style::common::muted_style;
use crate::ui::style::form::hdivider;
const LABEL_W: f32 = 120.0;
@@ -139,12 +141,6 @@ pub fn color_from_label(label: &str) -> Option<AcadColor> {
})
}
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
let palette = theme.palette();
let border = match status {
@@ -161,20 +157,6 @@ fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
}
}
/// Horizontal 1px divider in the shared border colour (matches style windows).
fn hdivider<'a>(width: Length) -> Element<'a, Message> {
container(Space::new().width(width).height(1))
.width(width)
.height(1)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color
)),
..Default::default()
})
.into()
}
/// One `label : widget` row with a fixed-width label column.
fn field_row<'a>(
label: Cow<'static, str>,
+76 -17
View File
@@ -264,7 +264,20 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
} else {
"No matches"
};
container(text(crate::t!(msg)).size(12).color(iced::Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }))
container(
text(crate::t!(msg))
.size(12)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(
theme
.palette()
.background
.base
.text
.scale_alpha(0.72),
),
}),
)
.center_x(Fill)
.center_y(Fill)
.width(Fill)
@@ -307,6 +320,56 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
.into()
}
/// Theme-aware foreground/background pair for a block card.
///
/// Centralizes the card's text/background mapping so the contrast suite can
/// exercise the exact colors the UI renders (instead of re-implementing the
/// match). Normal cards use the `base` pair, hovered/pressed cards use the
/// `strong` pair, and the actively-placing card uses the `primary.base` pair.
pub(crate) fn block_card_colors(
theme: &Theme,
is_placing: bool,
status: button::Status,
) -> (Color, Color) {
let palette = theme.palette();
if is_placing {
(palette.primary.base.color, palette.primary.base.text)
} else {
match status {
button::Status::Hovered | button::Status::Pressed => {
(palette.background.strong.color, palette.background.strong.text)
}
_ => (
palette.background.base.color,
palette.background.base.text,
),
}
}
}
/// Border color for a block card in a given state (exposed for contrast tests).
pub(crate) fn block_card_border(theme: &Theme, is_placing: bool) -> Color {
let palette = theme.palette();
if is_placing {
palette.primary.base.color
} else {
palette.background.neutral.color
}
}
/// Theme-aware foreground for the palette's header icon buttons.
///
/// Transparent resting state composites over the dock (`base`) background;
/// hovered/pressed state sits on the `strong` surface, so each state pairs
/// with its own background's text color to preserve contrast.
pub(crate) fn block_icon_button_text_color(theme: &Theme, status: button::Status) -> Color {
let palette = theme.palette();
match status {
button::Status::Hovered | button::Status::Pressed => palette.background.strong.text,
_ => palette.background.base.text,
}
}
fn block_card<'a>(palette: &'a BlockPalette, block: &'a BlockEntry) -> Element<'a, Message> {
let is_placing = palette.placing.as_deref() == Some(block.name.as_str());
let preview = canvas(BlockPreviewCanvas { wires: &block.wires })
@@ -315,9 +378,13 @@ fn block_card<'a>(palette: &'a BlockPalette, block: &'a BlockEntry) -> Element<'
// Fixed height guarantees the label occupies exactly one line, so every
// cell in a row keeps the same height even for long names (the elide budget
// handles overflow, and this clips anything that still won't fit).
//
// NOTE: no explicit `.color(...)` here — the label inherits the button's
// `text_color` so normal/hover/placing states each get a theme-aware
// foreground with sufficient contrast on both light and dark themes.
// (A hardcoded `Color::WHITE` is unreadable on light themes.)
let label = text(crate::ui::text_util::elide(&block.name, MAX_LABEL_CHARS))
.size(11)
.color(Color::WHITE)
.width(Fill)
.height(Length::Fixed(LABEL_LINE_H))
.center();
@@ -326,18 +393,8 @@ fn block_card<'a>(palette: &'a BlockPalette, block: &'a BlockEntry) -> Element<'
.on_press(Message::BlockPalette(BlockPaletteMsg::Insert(block.name.clone())))
.width(Fill)
.style(move |theme: &Theme, status| {
let palette = theme.palette();
let neutral = palette.background.neutral.color;
let base = palette.background.base.color;
let strong = palette.background.strong.color;
let (bg, border) = if is_placing {
(palette.primary.base.color, palette.primary.base.color)
} else {
match status {
button::Status::Hovered | button::Status::Pressed => (strong, neutral),
_ => (base, neutral),
}
};
let (bg, fg) = block_card_colors(theme, is_placing, status);
let border = block_card_border(theme, is_placing);
button::Style {
background: Some(Background::Color(bg)),
border: Border {
@@ -345,7 +402,7 @@ fn block_card<'a>(palette: &'a BlockPalette, block: &'a BlockEntry) -> Element<'
width: 1.0,
radius: 3.0.into(),
},
text_color: Color::WHITE,
text_color: fg,
..Default::default()
}
})
@@ -354,7 +411,9 @@ fn block_card<'a>(palette: &'a BlockPalette, block: &'a BlockEntry) -> Element<'
fn icon_button<'a>(icon: IconKind, msg: BlockPaletteMsg) -> Element<'a, Message> {
let icon_el: Element<'_, Message> = match icon {
IconKind::Glyph(s) => text(s).size(15).color(Color::WHITE).into(),
// No explicit color — inherits the button's theme-aware `text_color`
// so glyphs stay legible on light themes.
IconKind::Glyph(s) => text(s).size(15).into(),
IconKind::Svg(bytes) => crate::ui::icons::semantic(bytes, TOOL_H),
};
button(icon_el)
@@ -372,7 +431,7 @@ fn icon_button<'a>(icon: IconKind, msg: BlockPaletteMsg) -> Element<'a, Message>
radius: 3.0.into(),
..Default::default()
},
text_color: Color::WHITE,
text_color: block_icon_button_text_color(theme, status),
..Default::default()
})
.into()
+205
View File
@@ -0,0 +1,205 @@
//! The Browser: a standing outline of what the drawing is built from.
//!
//! Three sections, each read straight from the document rather than from a
//! parallel model the panel would have to keep in sync:
//!
//! * **Origin planes** — the three world planes, as somewhere to start a
//! sketch when the drawing is still empty and there is no face to pick.
//! * **Sketch** — the open sketch, if there is one.
//! * **Bodies** — every `Solid3D`, labelled with the operation that made it,
//! recovered from the DWG solid history.
//!
//! What this panel deliberately is *not* is a parametric feature tree. The
//! history records how each solid was built, but nothing re-runs it, so
//! editing an earlier feature cannot rebuild the ones after it. Showing the
//! list as if it were editable history would promise exactly that. It is a
//! table of contents, and it is labelled as one.
use crate::app::Message;
use crate::ui::dock::{DockMsg, PanelId};
use acadrust::objects::SolidHistoryOperation;
use acadrust::{CadDocument, EntityType, Handle};
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
use iced::{Background, Border, Element, Fill, Length, Theme};
/// The three world planes, with the argument `CREATESKETCH` takes for each.
const ORIGIN_PLANES: [(&str, &str); 3] = [("XY", "XY"), ("XZ", "XZ"), ("YZ", "YZ")];
/// Human name for the operation that produced a solid. `None` when the
/// drawing carries no history for it — a solid imported as plain geometry,
/// or one whose history was stripped on save.
fn operation_label(operation: &SolidHistoryOperation) -> &'static str {
match operation {
SolidHistoryOperation::Box(_) => "Box",
SolidHistoryOperation::Wedge(_) => "Wedge",
SolidHistoryOperation::Sphere(_) => "Sphere",
SolidHistoryOperation::Cone(_) => "Cone",
SolidHistoryOperation::Cylinder(_) => "Cylinder",
SolidHistoryOperation::Torus(_) => "Torus",
SolidHistoryOperation::Pyramid(_) => "Pyramid",
SolidHistoryOperation::Sweep(_) => "Sweep",
SolidHistoryOperation::Loft(_) => "Loft",
SolidHistoryOperation::Extrusion(_) => "Extrude",
SolidHistoryOperation::Revolve(_) => "Revolve",
SolidHistoryOperation::Fillet(_) => "Fillet",
SolidHistoryOperation::Chamfer(_) => "Chamfer",
_ => "Solid",
}
}
/// Every `Solid3D` in model space, with the label to show for it.
fn bodies(document: &CadDocument) -> Vec<(Handle, String)> {
document
.entities()
.filter(|entity| matches!(entity, EntityType::Solid3D(_)))
.map(|entity| {
let handle = entity.common().handle;
let label = crate::scene::model::solid_history::primitive_property_operation(
document, handle,
)
.as_ref()
.map_or("Solid", operation_label);
(handle, format!("{label} {:X}", handle.value()))
})
.collect()
}
fn section_header(label: String) -> Element<'static, Message> {
container(text(label).size(11))
.width(Fill)
.padding([3, 6])
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.into()
}
/// A clickable row that runs `command`.
fn entry_row(label: String, command: String) -> Element<'static, Message> {
button(text(label).size(12))
.on_press(Message::Command(command))
.width(Fill)
.padding([3, 10])
.style(button::subtle)
.into()
}
/// A row that states something rather than doing something.
fn note_row(label: String) -> Element<'static, Message> {
container(text(label).size(11).style(crate::ui::style::common::muted_style))
.width(Fill)
.padding([3, 10])
.into()
}
pub fn view<'a>(
document: &'a CadDocument,
open_sketch: Option<&'a str>,
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
// ── Dock chrome (title, pin, close) — matches the other panels ────────
let pin_icon = if auto_collapse {
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
} else {
crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
};
let pin = button(pin_icon)
.on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Browser)))
.style(move |theme: &Theme, status| {
let mut style = button::subtle(theme, status);
if auto_collapse {
let palette = theme.palette();
style.background = Some(Background::Color(palette.primary.weak.color));
style.text_color = palette.primary.weak.text;
style.border.color = palette.primary.base.color;
style.border.width = 1.0;
}
style
})
.padding([3, 5]);
let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4);
let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::Dock(DockMsg::Close(PanelId::Browser)))
.style(button::subtle)
.padding([3, 5]);
let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4);
let title_bar = mouse_area(
container(
row![
text(crate::t!("Browser")).size(12),
iced::widget::Space::new().width(Fill),
pin,
close,
]
.spacing(3)
.align_y(iced::Center),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.width(Fill)
.padding([3, 6]),
)
.on_press(Message::Dock(DockMsg::DockGrab(PanelId::Browser)))
.interaction(iced::mouse::Interaction::Grab);
let mut tree = column![].spacing(1);
tree = tree.push(section_header(crate::t!("Origin planes").into_owned()));
for (label, argument) in ORIGIN_PLANES {
tree = tree.push(entry_row(
label.to_string(),
format!("CREATESKETCH {argument}"),
));
}
tree = tree.push(section_header(crate::t!("Sketch").into_owned()));
match open_sketch {
Some(name) => {
tree = tree.push(entry_row(name.to_string(), "FINISHSKETCH".to_string()));
}
None => {
tree = tree.push(note_row(crate::t!("No sketch open").into_owned()));
}
}
let bodies = bodies(document);
tree = tree.push(section_header(crate::tf!("Bodies ({})", bodies.len()).into_owned()));
if bodies.is_empty() {
tree = tree.push(note_row(crate::t!("No solids yet").into_owned()));
} else {
for (handle, label) in bodies {
// Selecting by handle is what SELECT already takes, so clicking a
// row reuses the ordinary selection path.
tree = tree.push(entry_row(label, format!("SELECT {:X}", handle.value())));
}
}
let body = scrollable(container(tree).padding(iced::Padding {
top: 0.0,
right: 8.0,
bottom: 6.0,
left: 0.0,
}))
.width(Fill)
.height(Fill);
container(column![title_bar, body].spacing(6).padding(6))
.width(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()
})
.into()
}
+146 -98
View File
@@ -1,8 +1,8 @@
use crate::app::settings::IsoPlane;
use crate::app::Message;
use crate::snap::{SnapType, ALL_SNAP_MODES};
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::snap::{SnapType, ALL_3D_SNAP_MODES, ALL_SNAP_MODES};
use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Element, Fill, Theme};
use std::borrow::Cow;
/// Active tab in the Drafting Settings dialog.
@@ -28,6 +28,14 @@ pub struct DraftingSettingsState {
// Snap and Grid
pub snap_on: bool,
pub grid_on: bool,
pub snap_x_input: String,
pub snap_y_input: String,
pub snap_equal: bool,
pub grid_x_input: String,
pub grid_y_input: String,
pub grid_major_input: String,
pub grid_adaptive: bool,
pub grid_beyond_limits: bool,
pub isometric: bool,
pub iso_plane: IsoPlane,
pub snap_angle_deg: f32,
@@ -41,6 +49,7 @@ pub struct DraftingSettingsState {
pub snap_modes: rustc_hash::FxHashSet<SnapType>,
// 3D Object Snap
pub osnap3d_on: bool,
pub snap3d_modes: rustc_hash::FxHashSet<SnapType>,
// Dynamic Input
pub dyn_input_on: bool,
// Quick Properties
@@ -55,6 +64,14 @@ impl DraftingSettingsState {
pub fn is_dirty(&self, saved: &Self) -> bool {
self.snap_on != saved.snap_on
|| self.grid_on != saved.grid_on
|| self.snap_x_input != saved.snap_x_input
|| self.snap_y_input != saved.snap_y_input
|| self.snap_equal != saved.snap_equal
|| self.grid_x_input != saved.grid_x_input
|| self.grid_y_input != saved.grid_y_input
|| self.grid_major_input != saved.grid_major_input
|| self.grid_adaptive != saved.grid_adaptive
|| self.grid_beyond_limits != saved.grid_beyond_limits
|| self.isometric != saved.isometric
|| self.iso_plane != saved.iso_plane
|| (self.snap_angle_deg - saved.snap_angle_deg).abs() > 0.001
@@ -65,12 +82,38 @@ impl DraftingSettingsState {
|| self.otrack_on != saved.otrack_on
|| self.snap_modes != saved.snap_modes
|| self.osnap3d_on != saved.osnap3d_on
|| self.snap3d_modes != saved.snap3d_modes
|| self.dyn_input_on != saved.dyn_input_on
|| self.quick_props_on != saved.quick_props_on
|| self.selection_cycling_on != saved.selection_cycling_on
}
}
/// Format a snap spacing value for the dialog's text inputs.
pub fn format_snap_spacing(v: f32) -> String {
if (v - v.round()).abs() < 1e-4 {
format!("{}", v.round() as i32)
} else {
format!("{v}")
}
}
/// Parse a snap spacing input. Spacings must be positive and finite.
pub fn parse_snap_spacing(s: &str) -> Option<f32> {
let v: f32 = s.trim().parse().ok()?;
if v.is_finite() && v > 0.0 && v <= 1e9 {
Some(v)
} else {
None
}
}
/// Parse a major-line interval. Must be an integer in 2..=100.
pub fn parse_grid_major(s: &str) -> Option<u32> {
let v: u32 = s.trim().parse().ok()?;
(2..=100).contains(&v).then_some(v)
}
/// Helper for grouped sub-panels with a light border and header title.
fn group<'a>(title: impl Into<String>, body: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(
@@ -111,19 +154,44 @@ pub fn view_window<'a>(
};
// ── Tab Bar ──────────────────────────────────────────────────────────
// Real tab strip: a single non-wrapping row of tab-shaped buttons with a
// divider underneath, matching the style-manager editor shell. The row
// never wraps — it clips on overflow so "Selection Cycling" can't spill
// onto a second row.
let tab_button = |tab: DraftingSettingsTab, label: Cow<'a, str>| {
let is_active = state.active_tab == tab;
button(text(label).size(11.5))
button(text(label).size(11))
.on_press(Message::DraftingSettingsTabChanged(tab))
.style(if is_active {
button::primary
} else {
button::secondary
.style(move |theme: &Theme, status| {
let palette = theme.palette();
let pair = match (is_active, status) {
(true, _) => palette.primary.strong,
(
false,
button::Status::Hovered | button::Status::Pressed,
) => palette.background.strong,
_ => palette.background.weak,
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: iced::border::Radius {
top_left: 4.0,
top_right: 4.0,
bottom_right: 0.0,
bottom_left: 0.0,
},
},
..Default::default()
}
})
.padding([5, 9])
.padding([4, 8])
};
let tab_bar = row![
let tab_row = row![
tab_button(DraftingSettingsTab::SnapAndGrid, crate::t!("Snap and Grid")),
tab_button(DraftingSettingsTab::PolarTracking, crate::t!("Polar Tracking")),
tab_button(DraftingSettingsTab::ObjectSnap, crate::t!("Object Snap")),
@@ -132,8 +200,21 @@ pub fn view_window<'a>(
tab_button(DraftingSettingsTab::QuickProperties, crate::t!("Quick Properties")),
tab_button(DraftingSettingsTab::SelectionCycling, crate::t!("Selection Cycling")),
]
.spacing(4)
.wrap();
.spacing(2)
.width(Fill)
.clip(true);
let tab_divider = container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color,
)),
..Default::default()
});
let tab_bar = column![tab_row, tab_divider].spacing(0).width(Fill);
// ── Tab 1: Snap and Grid ─────────────────────────────────────────────
let snap_and_grid_view = {
@@ -149,18 +230,28 @@ pub fn view_window<'a>(
column![
row![
text(crate::t!("Snap X spacing:")).size(11).width(120),
text("10.00").size(11),
text_input("10", &state.snap_x_input)
.on_input(Message::DraftingSettingsSnapXChanged)
.size(11)
.padding([4, 7])
.width(100),
]
.spacing(8)
.align_y(iced::Center),
row![
text(crate::t!("Snap Y spacing:")).size(11).width(120),
text("10.00").size(11),
text_input("10", &state.snap_y_input)
.on_input(Message::DraftingSettingsSnapYChanged)
.size(11)
.padding([4, 7])
.width(100),
]
.spacing(8)
.align_y(iced::Center),
row![
checkbox(true).size(14),
checkbox(state.snap_equal)
.on_toggle(|_| Message::DraftingSettingsToggleEqualSnap)
.size(14),
text(crate::t!("Equal X and Y spacing")).size(11),
]
.spacing(6)
@@ -223,19 +314,31 @@ pub fn view_window<'a>(
column![
row![
text(crate::t!("Grid X spacing:")).size(11).width(120),
text("10.00").size(11),
text_input("10", &state.grid_x_input)
.on_input(Message::DraftingSettingsGridXChanged)
.size(11)
.padding([4, 7])
.width(100),
]
.spacing(8)
.align_y(iced::Center),
row![
text(crate::t!("Grid Y spacing:")).size(11).width(120),
text("10.00").size(11),
text_input("10", &state.grid_y_input)
.on_input(Message::DraftingSettingsGridYChanged)
.size(11)
.padding([4, 7])
.width(100),
]
.spacing(8)
.align_y(iced::Center),
row![
text(crate::t!("Major line every:")).size(11).width(120),
text("5").size(11),
text_input("5", &state.grid_major_input)
.on_input(Message::DraftingSettingsGridMajorChanged)
.size(11)
.padding([4, 7])
.width(100),
]
.spacing(8)
.align_y(iced::Center),
@@ -247,13 +350,17 @@ pub fn view_window<'a>(
crate::t!("Grid behavior"),
column![
row![
checkbox(true).size(14),
checkbox(state.grid_adaptive)
.on_toggle(|_| Message::DraftingSettingsToggleAdaptiveGrid)
.size(14),
text(crate::t!("Adaptive grid")).size(11),
]
.spacing(6)
.align_y(iced::Center),
row![
checkbox(true).size(14),
checkbox(state.grid_beyond_limits)
.on_toggle(|_| Message::DraftingSettingsToggleBeyondLimits)
.size(14),
text(crate::t!("Display grid beyond Limits")).size(11),
]
.spacing(6)
@@ -398,44 +505,26 @@ pub fn view_window<'a>(
Message::DraftingSettingsToggle3dOsnap,
);
let placeholder_group = group(
crate::t!("3D Object Snap modes"),
column![
row![checkbox(false).size(14), text(crate::t!("Vertex")).size(11)]
.spacing(7)
.align_y(iced::Center),
// Implemented 3D modes follow the same checkbox pattern as the
// Object Snap tab, driven by ALL_3D_SNAP_MODES.
let mut modes_col = column![].spacing(6);
for &(snap_type, _, label) in ALL_3D_SNAP_MODES {
let is_checked = state.snap3d_modes.contains(&snap_type);
modes_col = modes_col.push(
row![
checkbox(false).size(14),
text(crate::t!("Midpoint on edge")).size(11),
checkbox(is_checked)
.on_toggle(move |_| Message::DraftingSettingsToggleSnapMode3d(snap_type))
.size(14),
text(crate::t!(label)).size(11),
]
.spacing(7)
.align_y(iced::Center),
row![
checkbox(false).size(14),
text(crate::t!("Center of face")).size(11),
]
.spacing(7)
.align_y(iced::Center),
row![checkbox(false).size(14), text(crate::t!("Knot")).size(11)]
.spacing(7)
.align_y(iced::Center),
row![
checkbox(false).size(14),
text(crate::t!("Perpendicular to face")).size(11),
]
.spacing(7)
.align_y(iced::Center),
row![
checkbox(false).size(14),
text(crate::t!("Nearest to face")).size(11),
]
.spacing(7)
.align_y(iced::Center),
]
.spacing(6),
);
);
}
column![osnap3d_toggle, Space::new().height(4), placeholder_group]
let modes_group = group(crate::t!("3D Object Snap modes"), modes_col);
column![osnap3d_toggle, Space::new().height(4), modes_group]
.spacing(10)
.width(Fill)
};
@@ -628,51 +717,10 @@ pub fn view_window<'a>(
if !close_confirm {
return main_content.into();
}
let shield = iced::widget::mouse_area(
container(Space::new())
.width(Length::Fill)
.height(Length::Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme
.palette()
.background
.strongest
.color
.scale_alpha(0.55),
)),
..Default::default()
}),
)
.on_press(Message::DraftingSettingsCloseKeep);
let warning_panel = container(
column![
text(crate::t!("Unsaved changes will be discarded.")).size(13.5),
Space::new().height(12),
row![
button(text(crate::t!("Discard && close")).size(12))
.on_press(Message::DraftingSettingsCloseDiscard)
.padding([5, 14])
.style(button::danger),
button(text(crate::t!("Keep editing")).size(12))
.on_press(Message::DraftingSettingsCloseKeep)
.padding([5, 14])
.style(button::secondary),
]
.spacing(10),
]
.spacing(0),
)
.padding([18, 22])
.style(container::rounded_box);
iced::widget::stack![
crate::ui::modal::discard_guard(
main_content,
shield,
container(warning_panel).center_x(Fill).center_y(Fill)
]
.into()
Message::DraftingSettingsCloseDiscard,
Message::DraftingSettingsCloseKeep,
)
}
+1 -6
View File
@@ -2,6 +2,7 @@
use crate::app::Message;
use crate::ui::properties::{lw_options, LinetypeItem, LwItem};
use crate::ui::style::common::muted_style;
use crate::ui::ROW_H;
use acadrust::tables::layer::Layer as DocLayer;
use acadrust::tables::Table;
@@ -51,12 +52,6 @@ const LINETYPE_MENU_W: f32 = 220.0;
/// scrolled into view after it is added (#271).
pub const LAYER_TABLE_SCROLL_ID: &str = "layer-manager-table-scroll";
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn table_input_style(
theme: &Theme,
status: iced::widget::text_input::Status,
+2 -32
View File
@@ -1,16 +1,12 @@
//! Layout Manager window — fills the entire OS window.
use crate::app::Message;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{hdivider, vsep};
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Theme};
use crate::t;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn primary_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().primary.base.color),
@@ -37,32 +33,6 @@ fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
}
}
fn hdivider<'a>(width: iced::Length) -> Element<'a, Message> {
container(Space::new().width(width).height(1))
.width(width)
.height(1)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color,
)),
..Default::default()
})
.into()
}
fn vsep<'a>(height: iced::Length) -> Element<'a, Message> {
container(Space::new().width(1).height(height))
.width(1)
.height(height)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color,
)),
..Default::default()
})
.into()
}
pub fn view_window<'a>(
layouts: Vec<String>,
selected: &'a str,
+84
View File
@@ -0,0 +1,84 @@
// Missing-fonts prompt — shown after a drawing opens with `.shx` fonts this
// machine doesn't have. Offers to fetch them from the community repository
// (see `fonts/README.md` in the repo root) instead of rendering with a
// mismatched substitute.
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Element, Fill, Length, Shrink, Theme};
fn muted_style(theme: &Theme) -> text::Style {
text::Style {
color: Some(theme.palette().background.weak.text),
}
}
/// The prompt: the missing-font list, a download action, and a skip.
pub fn view_window<'a>(
fonts: &'a [String],
font_source: &'a str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let content_width = if matches!(sizing.width, Length::Fill) {
Fill
} else {
Shrink
};
let content_height = if matches!(sizing.height, Length::Fill) {
Fill
} else {
Shrink
};
let listed: Vec<Element<'a, Message>> = fonts
.iter()
.map(|name| text(format!(" {name}")).size(12).into())
.collect();
let actions = row![
Space::new().width(content_width),
button(text(crate::t!("Skip")).size(12))
.on_press(Message::MissingFontsDismiss)
.style(button::secondary)
.padding([6, 14]),
button(text(crate::t!("Download")).size(12))
.on_press(Message::MissingFontsDownload)
.style(button::primary)
.padding([6, 14]),
]
.spacing(8);
container(
column![
text(crate::t!("Missing fonts")).size(20),
text(crate::t!(
"This drawing uses fonts that are not installed on this machine. Download them from the OpenCADStudio community font repository?"
))
.size(11)
.style(muted_style),
container(scrollable(column(listed).spacing(2)).height(content_height))
.padding([10, 12])
.width(content_width)
.height(content_height)
.style(container::bordered_box),
column![
text(crate::t!("Font source — leave empty for the community repository, or point at any folder (e.g. your company's font server):"))
.size(10)
.style(muted_style),
text_input(
crate::t!("https://server/fonts or a GitHub raw folder").as_ref(),
font_source,
)
.on_input(Message::MissingFontsSourceChanged)
.size(11)
.padding([4, 8])
.width(content_width),
]
.spacing(3),
actions,
]
.spacing(10)
.width(sizing.width)
.height(sizing.height),
)
.padding([12, 16])
.into()
}
+4
View File
@@ -1,5 +1,6 @@
pub mod about;
pub mod block_palette;
pub mod browser;
pub mod layout_manager;
pub mod layer_state_manager;
pub mod drawing_units;
@@ -14,6 +15,7 @@ pub mod shortcuts;
pub mod layers;
pub mod update_notice;
pub mod open_progress;
pub mod missing_fonts;
pub mod recovery;
pub mod options;
pub mod attribute_editor;
@@ -21,3 +23,5 @@ pub mod annotation_data;
pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;
pub mod xref_help;
pub mod xref_manager;
+1 -6
View File
@@ -17,6 +17,7 @@ use crate::app::Message;
use crate::scene::named_parameters::ParameterTable;
use crate::scene::Scene;
use crate::t;
use crate::ui::style::common::muted_style;
use acadrust::types::Handle;
use acadrust::EntityType;
use iced::widget::tooltip::Position as TipPos;
@@ -113,12 +114,6 @@ pub struct ParamEditorRow {
/// column — same convention and value as `alias_editor::GUTTER`.
const GUTTER: f32 = 16.0;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn danger_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().danger.base.color),
+145 -72
View File
@@ -1,5 +1,6 @@
pub(crate) mod spacemouse;
use crate::app::config::UiThemeConfig;
use crate::app::settings::CursorType;
use crate::app::settings::{CursorType, RightClickMode};
use crate::app::Message;
use iced::widget::{
button, column, container, row, scrollable, slider, text, text_input, Space,
@@ -72,6 +73,12 @@ pub struct AppPrefs {
pub show_ucs_icon: bool,
/// UCSICON ORigin: draw it at the origin rather than the corner.
pub ucs_icon_at_origin: bool,
/// SHORTCUTMENU: what a right-click in the drawing area does.
pub right_click_mode: RightClickMode,
/// SHORTCUTMENUDURATION: time-sensitive hold threshold, ms.
pub right_click_hold_ms: i32,
/// Open the Plot / Page Setup dialog for every new layout.
pub page_setup_on_new_layout: bool,
}
/// The fixed locations the Files page lists.
@@ -141,6 +148,43 @@ impl<T> fmt::Display for Labelled<T> {
}
}
/// A background swatch that opens the colour wheel when clicked.
///
/// The wheel sits alongside the hex field rather than replacing it: typing
/// `#1E1E1E` stays the fastest way to reproduce an exact colour, while the
/// wheel is for choosing one by eye.
fn bg_swatch_picker<'a>(
target: crate::app::BgTarget,
rgb: [u8; 3],
open: Option<crate::app::BgTarget>,
) -> Element<'a, Message> {
let colour = iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2]);
let swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(colour)),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let underlay = button(swatch)
.on_press(Message::BgPickerOpen(target))
.padding(0)
.style(button::text);
iced_aw::ColorPicker::new(
open == Some(target),
colour,
underlay,
Message::BgPickerCancel,
Message::BgPickerSubmit,
)
.into()
}
#[allow(clippy::too_many_arguments)]
pub fn view_window<'a>(
default_save_format: &'a str,
@@ -153,6 +197,7 @@ pub fn view_window<'a>(
cursor_size: i32,
selection: SelectionPrefs,
prefs: AppPrefs,
spacemouse: Element<'a, Message>,
snap_angle_input: &'a str,
drawing_prefs: DrawingPrefs,
folders: Folders,
@@ -166,6 +211,9 @@ pub fn view_window<'a>(
model_bg_input: &'a str,
paper_bg_input: &'a str,
desk_bg_input: &'a str,
bg_picker: Option<crate::app::BgTarget>,
dirty: bool,
close_confirm: bool,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
@@ -173,7 +221,7 @@ pub fn view_window<'a>(
.copied()
.find(|candidate| *candidate == default_save_format);
let theme_options = Theme::ALL
let theme_options = crate::app::config::all_themes()
.iter()
.map(ToString::to_string)
.chain(std::iter::once("Custom".to_string()))
@@ -216,6 +264,18 @@ pub fn view_window<'a>(
.find(|choice| choice.value == cursor_type)
.cloned();
let right_click_options = RightClickMode::ALL
.into_iter()
.map(|value: RightClickMode| Labelled {
label: crate::t!(value.label()).into_owned(),
value,
})
.collect::<Vec<_>>();
let selected_right_click = right_click_options
.iter()
.find(|choice| choice.value == prefs.right_click_mode)
.cloned();
let palette = ui_theme.palette.to_iced();
let colors = [
(crate::tr!("options", "color-background"), palette.background),
@@ -253,8 +313,18 @@ pub fn view_window<'a>(
);
}
// Changes show at once but are committed by OK / Apply; Close puts them
// back (asking first when there is something to lose).
let ok = button(text(crate::t!("OK")).size(12))
.on_press(Message::OptionsOk)
.padding([6, 18])
.style(button::primary);
let apply = button(text(crate::t!("Apply")).size(12))
.on_press_maybe(dirty.then_some(Message::OptionsApply))
.padding([6, 18])
.style(if dirty { button::secondary } else { button::text });
let close = button(text(crate::tr!("action", "close")).size(12))
.on_press(Message::CloseModal)
.on_press(Message::OptionsClose)
.padding([6, 18])
.style(button::secondary);
@@ -305,6 +375,29 @@ pub fn view_window<'a>(
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(22),
text(crate::t!("Plotting")).size(15),
Space::new().height(10),
row![
text(crate::t!("Plot device, paper, scale and plot styles"))
.size(12)
.width(Fill),
button(text(crate::t!("Plot and Page Setup…")).size(11))
.on_press(Message::PlotDialogOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.page_setup_on_new_layout)
.on_toggle(Message::PageSetupOnNewLayoutChanged)
.size(15),
text(crate::t!("Show the page setup for new layouts")).size(12),
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
@@ -400,20 +493,6 @@ pub fn view_window<'a>(
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(22),
text(crate::t!("Plotting")).size(15),
Space::new().height(10),
row![
text(crate::t!("Plot device, paper, scale and plot styles"))
.size(12)
.width(Fill),
button(text(crate::t!("Plot and Page Setup…")).size(11))
.on_press(Message::PlotDialogOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
@@ -437,58 +516,10 @@ pub fn view_window<'a>(
});
let model_bg_rgb = model_space.custom_bg.unwrap_or(crate::app::config::CLASSIC_CAD_DARK_BG);
let model_bg_swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
model_bg_rgb[0],
model_bg_rgb[1],
model_bg_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let paper_bg_rgb = model_space.custom_paper_bg.unwrap_or(crate::app::config::DEFAULT_PAPER_BG);
let paper_bg_swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
paper_bg_rgb[0],
paper_bg_rgb[1],
paper_bg_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let desk_bg_rgb = model_space.custom_desk_bg.unwrap_or(crate::app::config::DEFAULT_DESK_BG);
let desk_bg_swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
desk_bg_rgb[0],
desk_bg_rgb[1],
desk_bg_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let mode_options = crate::app::config::ModelSpaceMode::ALL
.into_iter()
@@ -623,7 +654,7 @@ pub fn view_window<'a>(
display = display.push(Space::new().height(10)).push(
row![
text(crate::t!("Model background")).size(12).width(140),
model_bg_swatch,
bg_swatch_picker(crate::app::BgTarget::Model, model_bg_rgb, bg_picker),
text_input("#RRGGBB", model_bg_input)
.on_input(Message::ModelSpaceBgChanged)
.width(150),
@@ -636,7 +667,7 @@ pub fn view_window<'a>(
display = display.push(Space::new().height(10)).push(
row![
text(crate::t!("Paper background")).size(12).width(140),
paper_bg_swatch,
bg_swatch_picker(crate::app::BgTarget::Paper, paper_bg_rgb, bg_picker),
text_input("#RRGGBB", paper_bg_input)
.on_input(Message::PaperSpaceBgChanged)
.width(150),
@@ -648,7 +679,7 @@ pub fn view_window<'a>(
display = display.push(Space::new().height(10)).push(
row![
text(crate::t!("Desk surround")).size(12).width(140),
desk_bg_swatch,
bg_swatch_picker(crate::app::BgTarget::Desk, desk_bg_rgb, bg_picker),
text_input("#RRGGBB", desk_bg_input)
.on_input(Message::DeskSpaceBgChanged)
.width(150),
@@ -1135,6 +1166,8 @@ pub fn view_window<'a>(
let user_prefs = column![
text(crate::t!("User Preferences")).size(15),
Space::new().height(10),
spacemouse,
Space::new().height(12),
text(crate::t!("Zoom")).size(15),
Space::new().height(10),
row![
@@ -1217,6 +1250,43 @@ pub fn view_window<'a>(
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Right-click Customization")).size(15),
Space::new().height(10),
row![
text(crate::t!("Right-click in drawing area")).size(12).width(150),
iced::widget::pick_list(
selected_right_click,
right_click_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::RightClickModeChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(12),
row![
text(crate::t!("Hold duration")).size(12).width(150),
slider(
100..=1000,
prefs.right_click_hold_ms.clamp(100, 1000),
Message::RightClickHoldMsChanged
)
.step(50)
.width(Fill),
text(format!("{} ms", prefs.right_click_hold_ms.clamp(100, 1000)))
.size(11)
.width(52),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Block Edit")).size(15),
Space::new().height(10),
row![
@@ -1610,7 +1680,7 @@ pub fn view_window<'a>(
// controls at the trailing edge of the Options content.
scrollable(content).spacing(8).height(Fill),
Space::new().height(12),
row![Space::new().width(Fill), close],
row![Space::new().width(Fill), ok, apply, close].spacing(8),
]
.width(Fill)
.height(sizing.height);
@@ -1620,10 +1690,13 @@ pub fn view_window<'a>(
.height(sizing.height);
let intrinsic = sizing.width == crate::ui::modal::ModalSizing::INTRINSIC.width;
container(body)
let main = container(body)
.style(container::rounded_box)
.padding([16, 18])
.width(if intrinsic { iced::Length::Fixed(DIALOG_WIDTH) } else { sizing.width })
.height(if intrinsic { iced::Length::Fixed(DIALOG_HEIGHT) } else { sizing.height })
.into()
.height(if intrinsic { iced::Length::Fixed(DIALOG_HEIGHT) } else { sizing.height });
if !close_confirm {
return main.into();
}
crate::ui::modal::discard_guard(main, Message::OptionsCloseDiscard, Message::OptionsCloseKeep)
}
+135
View File
@@ -0,0 +1,135 @@
use crate::app::Message;
use crate::input::spacemouse::{NavigationMode, Preferences, Status};
use iced::widget::{button, checkbox, column, container, pick_list, row, slider, text, Space};
use iced::{Element, Fill};
pub(crate) const ICON: &[u8] = include_bytes!("../../../../assets/icons/ui/spacemouse.svg");
pub(crate) fn view(
preferences: Preferences,
status: Status,
paused: bool,
details: bool,
) -> Element<'static, Message> {
let supported = cfg!(windows);
let mut enabled = checkbox(preferences.enabled).size(15);
if supported {
enabled = enabled.on_toggle(Message::SpaceMouseEnabled);
}
let mode = pick_list(Some(preferences.mode), NavigationMode::ALL, |mode| {
crate::t!(mode.label()).into_owned()
})
.width(220);
let mode = if supported {
mode.on_select(Message::SpaceMouseMode)
} else {
mode
};
let status_text = if !preferences.enabled {
crate::t!("Disabled").into_owned()
} else if paused {
crate::t!("Paused").into_owned()
} else {
crate::t!(status.label()).into_owned()
};
let mut content = column![
row![
crate::ui::icons::themed(ICON, 22.),
text(crate::t!("SpaceMouse")).size(15)
]
.spacing(8)
.align_y(iced::Center),
row![enabled, text(crate::t!("Enable SpaceMouse")).size(12)]
.spacing(8)
.align_y(iced::Center),
row![
text(crate::t!("Status")).size(12).width(150),
text(status_text).size(12)
]
.spacing(10),
row![text(crate::t!("Navigation")).size(12).width(150), mode]
.spacing(10)
.align_y(iced::Center),
text(crate::t!(preferences.mode.description()).into_owned())
.size(11)
.width(Fill),
]
.spacing(12);
if preferences.mode != NavigationMode::Full3D && supported {
content = content.push(
row![
checkbox(preferences.pan_reversed)
.size(15)
.on_toggle(Message::SpaceMousePanReversed),
text(crate::t!("Reverse pan direction")).size(12),
]
.spacing(8)
.align_y(iced::Center),
);
content = content.push(
row![
text(crate::t!("Pan speed")).size(12).width(150),
slider(
10..=300,
preferences.pan_speed.clamp(10, 300),
Message::SpaceMousePanSpeed
)
.step(10)
.width(160),
text(format!("{}%", preferences.pan_speed.clamp(10, 300))).size(11),
]
.spacing(10)
.align_y(iced::Center),
);
}
if paused {
content = content.push(
button(text(crate::t!("Resume SpaceMouse")).size(11))
.on_press(Message::SpaceMousePause)
.style(button::secondary),
);
}
content = content.push(
button(
text(crate::t!(if supported {
"Open 3Dconnexion settings…"
} else {
"SpaceMouse information…"
}))
.size(11),
)
.on_press(Message::SpaceMouseDriverSettings)
.style(button::secondary)
.padding([5, 10]),
);
content = content.push(
text(crate::t!(
"Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here."
))
.size(11)
.width(Fill),
);
content = content.push(
button(
row![
crate::ui::icons::themed_arrow_toggle(details, 9.),
text(crate::t!("Connection details")).size(11)
]
.spacing(6),
)
.on_press(Message::SpaceMouseDetails)
.style(button::text),
);
if details {
let detail = match status {
Status::Unavailable(reason) => reason,
Status::Unsupported => "This version supports SpaceMouse through 3DxWare on Windows. Other platforms do not yet have a device adapter.".into(),
Status::Disconnected => "3DxWare is running. Connect a SpaceMouse; Open CAD Studio reconnects automatically.".into(),
_ => "Uses the installed 3DxWare driver. Navigation pauses while a dialog is open. Your selected navigation mode is saved across drawings and sessions.".into(),
};
content = content.push(text(crate::t!(&detail).into_owned()).size(11).width(Fill));
}
container(content.push(Space::new().height(8)))
.width(Fill)
.into()
}
+823 -173
View File
File diff suppressed because it is too large. Load diff
+1 -6
View File
@@ -8,6 +8,7 @@
use crate::app::Message;
use crate::plugin::external::{ExternalPlugin, RegistryEntry, ReleaseInfo};
use crate::ui::style::common::muted_style;
use iced::widget::{
button, column, container, markdown, row, rule, scrollable, text, text_input, Space,
};
@@ -48,12 +49,6 @@ inventory::submit!(crate::command::CommandRegistration {
names: &["PLUGINS", "PLUGINMANAGER"]
});
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn primary_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().primary.base.color),
+1 -6
View File
@@ -1,5 +1,6 @@
use crate::app::Message;
use crate::t;
use crate::ui::style::common::muted_style as muted;
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Length, Theme};
@@ -13,12 +14,6 @@ fn button_style(primary: bool) -> impl Fn(&Theme, button::Status) -> button::Sty
}
}
fn muted(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
pub fn view_window<'a>(
layouts: &'a [(String, bool)],
printer: Option<&'a str>,
+1 -6
View File
@@ -1,14 +1,9 @@
use crate::app::Message;
use crate::io::recovery::{RecoveryReport, RecoveryStatus};
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Fill, Length, Shrink, Theme};
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn status_style(
status: RecoveryStatus,
) -> impl Fn(&Theme) -> iced::widget::text::Style + Copy {
+1 -6
View File
@@ -2,6 +2,7 @@
use crate::app::Message;
use crate::t;
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Length, Theme};
@@ -13,12 +14,6 @@ pub enum ShortcutField {
const GUTTER: f32 = 16.0;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn danger_text_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().danger.base.color),
+1 -6
View File
@@ -1,15 +1,10 @@
use crate::app::Message;
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
fn primary_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().primary.base.color),
+97
View File
@@ -0,0 +1,97 @@
//! Reference Manager help window: how the manager works on the web build
//! (read-only list, disabled mutations, no previews) and where to report
//! issues. Rendered as a modal like About/DSettings so the text gets a
//! proper window width — an anchored toolbar dropdown squeezed it into an
//! unreadable sliver.
use crate::app::Message;
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, text, Space};
use iced::{Background, Element, Fill, Length, Theme};
/// GitHub issues page linked from the help window, so web users can report
/// missing or broken behavior where it will be seen.
pub const XREF_ISSUES_URL: &str = "https://github.com/HakanSeven12/OpenCADStudio/issues";
pub fn view_window(sizing: crate::ui::modal::ModalSizing) -> Element<'static, Message> {
// Fixed window width: paragraphs wrap against this instead of the
// toolbar, which is what made the dropdown unreadable.
let body_w = Length::Fixed(460.0);
let para = |label: String| -> Element<'static, Message> {
text(label).size(12).width(Fill).into()
};
let intro = para(crate::t!("How the Reference Manager works on web").into_owned());
let list = para(
crate::t!(
"This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation."
)
.into_owned(),
);
let limits = para(
crate::t!(
"Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed — and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first."
)
.into_owned(),
);
let report_hint = para(
crate::t!(
"Hit something broken or missing? Report it on the GitHub issues page so it can be fixed."
)
.into_owned(),
);
let report_button = button(text(crate::t!("Report an issue")).size(12))
.on_press(Message::OpenUrl(XREF_ISSUES_URL.to_string()))
.style(button::primary)
.padding([6, 18]);
let close_button = button(text(crate::t!("Close")).size(12))
.on_press(Message::CloseModal)
.style(button::secondary)
.padding([6, 18]);
let action_row: Element<'static, Message> = row![
Space::new().width(Fill),
report_button,
close_button,
]
.spacing(8)
.align_y(iced::Center)
.into();
let content = column![
text(crate::t!("Reference Manager — Web").into_owned())
.size(14),
text(crate::t!("EXTERNALREFERENCES on the web build").into_owned())
.size(11)
.style(muted_style),
intro,
list,
limits,
report_hint,
Space::new().height(4),
action_row,
]
.spacing(10)
.padding(16)
.width(body_w);
container(content)
.width(sizing.width)
.height(sizing.height)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
..Default::default()
})
.into()
}
#[cfg(test)]
mod tests {
#[test]
fn issues_url_points_at_github_issues() {
assert!(super::XREF_ISSUES_URL.starts_with("https://github.com/"));
assert!(super::XREF_ISSUES_URL.contains("/issues"));
}
}
File diff suppressed because it is too large. Load diff