forked from 1b-agent/opencadstudio-upstream
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:
commit
8648225ac5
309 files changed
+81166
-4157
No files matched your search
@@ -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
@@ -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()
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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,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,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 {
|
||||
|
||||
@@ -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,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),
|
||||
|
||||
@@ -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
Reference in new issue
Block a user