Merge remote-tracking branch 'origin/main' into merge-test/main-into-constraints

# Conflicts:
#	src/app/command_driver.rs
#	src/command.rs
#	src/ui/ribbon/mod.rs
#	src/ui/window/options.rs
This commit is contained in:
Felix Riestra committed 2026-09-10 23:34:50 +02:00
commit ad718def3f
124 files changed
+9866 -692

No files matched your search

+5 -3
View File
@@ -790,7 +790,7 @@ fn draw_grip_marker(
b.close();
}),
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),
GripShape::Dropdown => canvas::Path::new(|b| {
GripShape::Dropdown | GripShape::DropdownAdjacent => canvas::Path::new(|b| {
b.move_to(Point::new(sp.x - h, sp.y - h * 0.5));
b.line_to(Point::new(sp.x + h, sp.y - h * 0.5));
b.line_to(Point::new(sp.x, sp.y + h));
@@ -840,7 +840,10 @@ fn draw_grip_marker(
} else {
palette.primary.base.color
};
let fill = if grip.shape == GripShape::Dropdown {
let fill = if matches!(
grip.shape,
GripShape::Dropdown | GripShape::DropdownAdjacent
) {
color
} else {
palette.background.base.color.scale_alpha(0.7)
@@ -3844,4 +3847,3 @@ mod selection_visual_color_tests {
assert!((selection_fill_alpha(100.0, true) - 0.45).abs() < 1e-5);
}
}
+261
View File
@@ -0,0 +1,261 @@
//! Additional drawing tools, reached from the Draw panel's title.
use std::time::Duration;
use iced::widget::{button, column, container, row, scrollable, text, tooltip};
use iced::{Element, Fill, Theme};
use super::widgets::{
make_icon, make_tip, muted_text_style, popup_panel_style, popup_row_style, tip_style,
tool_btn_style,
};
use super::{dropdown_backdrop, position_ribbon_dropdown, Ribbon};
use crate::app::Message;
use crate::modules::IconKind;
use crate::t;
use crate::ui::{icons, wrap_bar::PosReport};
const PANEL_ID: &str = "draw_extension";
const GROUP_ID: &str = "draw_extension_group";
const SCALE: f32 = 0.7;
const CELL: f32 = 36.0 * SCALE;
const GAP: f32 = 3.0 * SCALE;
const OPTION_HEIGHT: f32 = 30.0 * SCALE;
struct Tool {
command: &'static str,
label: &'static str,
icon: &'static [u8],
options: &'static [(&'static str, &'static str)],
}
const TOOLS: &[Tool] = &[
Tool {
command: "SPLINE",
label: "Spline Fit",
icon: include_bytes!("../../../assets/icons/spline.svg"),
options: &[],
},
Tool {
command: "SPLINECV",
label: "Spline CV",
icon: include_bytes!("../../../assets/icons/spline_cv.svg"),
options: &[],
},
Tool {
command: "XLINE",
label: "Construction Line",
icon: include_bytes!("../../../assets/icons/xline.svg"),
options: &[],
},
Tool {
command: "RAY",
label: "Ray",
icon: include_bytes!("../../../assets/icons/ray.svg"),
options: &[],
},
Tool {
command: "MULTIPOINT",
label: "Multiple Points",
icon: include_bytes!("../../../assets/icons/multipoint.svg"),
options: &[
("POINT", "Single Point"),
("MULTIPOINT", "Multiple Points"),
("DDPTYPE", "Point Style"),
],
},
Tool {
command: "DIVIDE",
label: "Divide",
icon: include_bytes!("../../../assets/icons/divide.svg"),
options: &[],
},
Tool {
command: "MEASURE",
label: "Measure",
icon: include_bytes!("../../../assets/icons/measure.svg"),
options: &[],
},
Tool {
command: "REGION",
label: "Region",
icon: include_bytes!("../../../assets/icons/region.svg"),
options: &[],
},
Tool {
command: "WIPEOUT",
label: "Wipeout",
icon: include_bytes!("../../../assets/icons/wipeout.svg"),
options: &[],
},
Tool {
command: "BOUNDARY",
label: "Boundary",
icon: include_bytes!("../../../assets/icons/boundary.svg"),
options: &[],
},
Tool {
command: "HELIX",
label: "Helix",
icon: include_bytes!("../../../assets/icons/helix.svg"),
options: &[],
},
Tool {
command: "DONUT",
label: "Donut",
icon: include_bytes!("../../../assets/icons/donut.svg"),
options: &[],
},
Tool {
command: "REVCLOUD",
label: "Revision Cloud",
icon: include_bytes!("../../../assets/icons/revcloud.svg"),
options: &[
("REVCLOUD_RECTANGULAR", "Rectangular"),
("REVCLOUD_POLYGONAL", "Polygonal"),
("REVCLOUD_FREEHAND", "Freehand"),
],
},
];
pub(super) fn owns_dropdown(id: &str) -> bool {
id == PANEL_ID
|| TOOLS
.iter()
.any(|tool| tool.command == id && !tool.options.is_empty())
}
pub(super) fn group_anchor<'a>(title: &str, content: Element<'a, Message>) -> Element<'a, Message> {
if title == "Draw" {
PosReport::new(GROUP_ID, content).into()
} else {
content
}
}
pub(super) fn group_title<'a>(title: &'static str, open: &Option<String>, font_size: f32) -> Element<'a, Message> {
if title != "Draw" || TOOLS.is_empty() {
return container(text(t!(title)).size(font_size).style(muted_text_style))
.padding([1, 4])
.into();
}
let expanded = open.as_deref().is_some_and(owns_dropdown);
let arrow = if expanded {
icons::themed_arrow_up(7.0)
} else {
icons::themed_arrow_down(7.0)
};
PosReport::new(
PANEL_ID,
button(
row![text(t!(title)).size(font_size), arrow]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(PANEL_ID.to_string()))
.style(move |theme: &Theme, status| tool_btn_style(theme, expanded, status))
.padding([1, 4]),
)
.into()
}
fn tool_button(tool: &Tool, active: bool) -> Element<'static, Message> {
let face = button(make_icon(IconKind::Svg(tool.icon), 23.0 * SCALE))
.on_press(Message::DropdownSelectItem {
dropdown_id: PANEL_ID,
cmd: tool.command,
})
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(if tool.options.is_empty() {
CELL
} else {
CELL - 11.0 * SCALE
})
.height(CELL)
.padding(3.0 * SCALE);
let tip = format!("{}\n{} {}", t!(tool.label), t!("Command:"), tool.command);
let face: Element<'static, Message> = tooltip(face, make_tip(tip), tooltip::Position::Bottom)
.delay(Duration::from_millis(400))
.style(tip_style)
.into();
if !tool.options.is_empty() {
row![
face,
button(icons::themed_arrow_down(6.0 * SCALE))
.on_press(Message::ToggleRibbonDropdown(tool.command.to_string()))
.style(move |theme: &Theme, status| tool_btn_style(theme, false, status))
.width(11.0 * SCALE)
.height(CELL)
.padding(2.0 * SCALE)
]
.into()
} else {
face
}
}
pub(super) fn overlay<'a>(ribbon: &Ribbon, id: &str, win: (f32, f32)) -> Element<'a, Message> {
let width = (7.0 * (CELL + GAP) - GAP + 12.0).min((win.0 - 8.0).max(CELL + 12.0));
let (_, x, anchor_top) = ribbon.dd_anchor(PANEL_ID, width, win.0);
let anchor = crate::ui::wrap_bar::dropdown_bounds(GROUP_ID);
let left = anchor
.map_or(x, |b| b.x)
.clamp(0.0, (win.0 - width).max(0.0));
let top = anchor_top.min((win.1 - 80.0).max(0.0));
let available_height = (win.1 - top - 4.0).max(1.0);
let cols = (((width - 12.0 + GAP) / (CELL + GAP)).floor() as usize).clamp(1, 7);
let option_tool = TOOLS
.iter()
.find(|tool| tool.command == id && !tool.options.is_empty());
let content_height = option_tool.map_or_else(
|| TOOLS.len().div_ceil(cols) as f32 * (CELL + GAP) - GAP + 12.0,
|tool| tool.options.len() as f32 * OPTION_HEIGHT,
);
let contents: Element<'static, Message> = if let Some(tool) = option_tool {
column(
tool.options
.iter()
.map(|&(cmd, label)| {
button(text(t!(label)).size(11))
.on_press(Message::DropdownSelectItem {
dropdown_id: tool.command,
cmd,
})
.style(popup_row_style)
.width(Fill)
.height(OPTION_HEIGHT)
.padding([3, 7])
.into()
})
.collect::<Vec<Element<'static, Message>>>(),
)
.into()
} else {
column(
TOOLS
.chunks(cols)
.map(|tools| {
row(tools
.iter()
.map(|tool| {
tool_button(tool, ribbon.active_tool.as_deref() == Some(tool.command))
})
.collect::<Vec<_>>())
.spacing(GAP)
.into()
})
.collect::<Vec<Element<'static, Message>>>(),
)
.spacing(GAP)
.padding(6)
.into()
};
let panel = container(scrollable(contents).height(content_height.min(available_height)))
.width(width)
.style(|theme: &Theme| {
let mut style = popup_panel_style(theme);
style.border.color = theme.palette().primary.base.color;
style
});
dropdown_backdrop(position_ribbon_dropdown(panel.into(), false, left, top))
}
+12 -9
View File
@@ -21,6 +21,7 @@ use crate::plugin::all_ribbon_modules;
use crate::ui::properties::{linetype_display_name, lw_options, LinetypeItem};
mod widgets;
mod draw_panel;
use widgets::{StyleContext, *};
mod collapse;
use collapse::{CollapsePanels, Panel};
@@ -369,6 +370,9 @@ impl Ribbon {
self.open_dropdown = None;
} else {
self.open_dropdown = Some(id.to_string());
if draw_panel::owns_dropdown(id) {
self.collapsed_open = None;
}
}
}
pub fn close_dropdown(&mut self) {
@@ -421,7 +425,7 @@ impl Ribbon {
pub fn select_dropdown_item(&mut self, dropdown_id: &'static str, cmd: &'static str) {
self.last_cmd.insert(dropdown_id, cmd);
self.open_dropdown = None;
self.close_dropdown();
}
// ── View ──────────────────────────────────────────────────────────────
@@ -761,6 +765,10 @@ impl Ribbon {
}
let open_id = self.open_dropdown.as_deref()?;
if draw_panel::owns_dropdown(open_id) {
return Some(draw_panel::overlay(self, open_id, win));
}
if open_id == UNDO_HISTORY_ID || open_id == REDO_HISTORY_ID {
let is_undo = open_id == UNDO_HISTORY_ID;
let labels = if is_undo { undo_labels } else { redo_labels };
@@ -1401,20 +1409,15 @@ fn render_group<'a>(
r.push(e)
});
column![
draw_panel::group_anchor(group.title, column![
tools_el,
container(
text(t!(group.title))
.size(group_title_font_size as f32)
.style(muted_text_style),
)
.padding([1, 4]),
draw_panel::group_title(group.title, open_dd, group_title_font_size as f32),
]
.align_x(iced::Center)
.spacing(0)
.padding([3u16, 4])
.height(Length::Fixed(widgets::tool_bar_h(group_title_font_size as f32)))
.into()
.into())
}
/// The top-level command id of a ribbon item, if it has one.
+753 -20
View File
@@ -7,13 +7,106 @@ use iced::widget::{
use iced::{Background, Border, Element, Fill, Theme};
use std::fmt;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
/// Width of the vertical tab rail. Wide enough for the longest translated
/// page name; German and Russian are the ones that set it.
const TAB_RAIL_WIDTH: f32 = 178.0;
/// The dialog's own size when the modal is laid out intrinsically.
const DIALOG_WIDTH: f32 = 880.0;
const DIALOG_HEIGHT: f32 = 620.0;
/// Which page of the Options dialog is showing.
///
/// Persisted: with nine pages, reopening on General every time means hunting
/// for the one you were last in.
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum OptionsTab {
#[default]
General,
Files,
OpenAndSave,
Display,
Drafting,
Modeling,
Selection,
Drawing,
UserPreferences,
}
/// Application preferences the dialog reads that are plain scalars on the app.
///
/// Gathered rather than passed one by one: `view_window` already carries a
/// long positional list, and several of these are adjacent `bool`s that would
/// swap silently at the call site.
#[derive(Clone, Copy)]
pub struct AppPrefs {
/// SAVETIME, minutes between recovery saves; 0 disables.
pub savetime_min: i32,
/// ISAVEBAK: keep a `.bak` when overwriting.
pub backup_on_save: bool,
/// TEXTFILL: fill TrueType glyphs rather than drawing them hollow.
pub textfill: bool,
/// CLIPROMPTLINES: prompt lines shown above the command window.
pub cliprompt_lines: i32,
/// COMMANDLINEFADETIME: how long overlay history lines stay visible, in ms.
pub commandline_fade_ms: i32,
/// ZOOMWHEEL: reverse the mouse-wheel zoom direction.
pub zoom_wheel_reversed: bool,
/// ZOOMFACTOR, 3..=100.
pub zoom_factor: i32,
/// TEXTEDITMODE: TEXTEDIT keeps prompting for the next object.
pub texteditmode: bool,
/// DIMCONTINUEMODE: continued dimensions inherit the base dimension's style.
pub dimension_continue_mode: i16,
/// QDIM extension-origin priority: 0 endpoints, 1 intersections.
pub qdim_snap_priority: u8,
/// ANNOAUTOSCALE, -4..=4. The sign is on/off; the magnitude selects which
/// objects a newly added scale reaches.
pub annotation_auto_scale: i8,
/// Polar tracking increment in degrees.
pub polar_increment_deg: f32,
/// NAVVCUBE: show the navigation cube.
pub show_viewcube: bool,
/// UCSICON: show the UCS icon.
pub show_ucs_icon: bool,
/// UCSICON ORigin: draw it at the origin rather than the corner.
pub ucs_icon_at_origin: bool,
}
/// The fixed locations the Files page lists.
///
/// `None` where the platform could not give one — the row then says so and
/// its button is disabled rather than opening nothing.
#[derive(Clone, Default)]
pub struct Folders {
pub config: Option<String>,
pub plot_styles: Option<String>,
pub plugins: Option<String>,
pub autosave: Option<String>,
}
/// Values read from the current drawing's header rather than from preferences.
///
/// These are saved in the DWG, so they follow the drawing rather than the
/// application — the page labels them as such. `available` is false when no
/// drawing is open and the controls are not drawn at all.
#[derive(Clone, Copy)]
pub struct DrawingPrefs {
pub available: bool,
/// ISOLINES.
pub isolines: i16,
/// DISPSILH.
pub display_silhouette: bool,
/// SURFU / SURFV.
pub surface_u: i16,
pub surface_v: i16,
/// SURFTYPE: 5, 6 or 8.
pub surface_type: i16,
/// SOLIDHIST.
pub record_solid_history: bool,
/// SHOWHIST, 0..=2.
pub show_solid_history: i16,
}
/// The Selection-card settings that live on `UserSettings` rather than on the
@@ -32,6 +125,8 @@ pub struct SelectionPrefs {
pub pick_drag_rect: bool,
/// GRIPOBJLIMIT, 0..=32767; 0 = no limit.
pub grip_object_limit: i32,
/// Selection cycling: a click where objects overlap opens a picker.
pub selection_cycling: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -58,6 +153,10 @@ pub fn view_window<'a>(
active_tab: OptionsTab,
cursor_size: i32,
selection: SelectionPrefs,
prefs: AppPrefs,
snap_angle_input: &'a str,
drawing_prefs: DrawingPrefs,
folders: Folders,
double_click_block_refedit: bool,
double_click_block_attedit: bool,
cursor_type: CursorType,
@@ -178,8 +277,50 @@ pub fn view_window<'a>(
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(22),
text(crate::tr!("options", "open-save-section")).size(15),
Space::new().height(24),
text(crate::t!("Applications")).size(15),
Space::new().height(10),
row![
text(crate::t!("Installed plugins and their sources")).size(12).width(Fill),
button(text(crate::t!("Plugins…")).size(11))
.on_press(Message::PluginManagerOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Keyboard shortcuts")).size(12).width(Fill),
button(text(crate::t!("Keyboard Shortcuts…")).size(11))
.on_press(Message::ShortcutsPanelOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Command aliases")).size(12).width(Fill),
button(text(crate::t!("Command Aliases…")).size(11))
.on_press(Message::AliasEditorOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
// Saving preferences, gathered onto one page. The format and the file
// association were on General, which had become a page of three unrelated
// controls; the autosave interval and the backup toggle have existed since
// #205 with no control at all.
let open_and_save = column![
text(crate::t!("Open and Save")).size(15),
Space::new().height(10),
Space::new().height(10),
row![
text(crate::tr!("options", "default-save-format-label")).size(12).width(150),
@@ -248,6 +389,53 @@ pub fn view_window<'a>(
))
.size(11)
.width(sizing.width),
Space::new().height(22),
text(crate::t!("File Safety")).size(15),
Space::new().height(10),
row![
text(crate::t!("Automatic save")).size(12).width(150),
slider(0..=120, prefs.savetime_min.clamp(0, 120), Message::SaveTimeChanged)
.step(1)
.width(Fill),
text(if prefs.savetime_min <= 0 {
crate::t!("Off").into_owned()
} else {
format!("{} min", prefs.savetime_min)
})
.size(11)
.width(52),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Minutes between recovery saves to a .sv$ file; 0 turns it off (SAVETIME)."
))
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
iced::widget::checkbox(prefs.backup_on_save)
.on_toggle(Message::BackupOnSaveChanged)
.size(15),
text(crate::t!("Keep a .bak copy when overwriting a drawing (ISAVEBAK)")).size(12),
]
.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);
@@ -615,6 +803,75 @@ pub fn view_window<'a>(
.size(11)
.width(sizing.width),
)
.push(Space::new().height(14))
.push(
row![
iced::widget::checkbox(prefs.textfill)
.on_toggle(Message::TextFillChanged)
.size(15),
text(crate::t!("Fill TrueType glyphs (TEXTFILL)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(24))
.push(text(crate::t!("Command Line")).size(15))
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Prompt lines")).size(12).width(140),
slider(
0..=50,
prefs.cliprompt_lines.clamp(0, 50),
Message::ClipromptLinesChanged,
)
.step(1)
.width(Fill),
text(prefs.cliprompt_lines.clamp(0, 50).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!(
"Temporary prompt lines shown above the command window (CLIPROMPTLINES)."
))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(12))
.push(
row![
text(crate::t!("History fade time")).size(12).width(140),
slider(
0..=60000,
prefs.commandline_fade_ms.clamp(0, 60000),
Message::CommandLineFadeChanged,
)
.step(250)
.width(Fill),
text(if prefs.commandline_fade_ms <= 0 {
crate::t!("Off").into_owned()
} else {
format!("{:.1} s", prefs.commandline_fade_ms as f32 / 1000.0)
})
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!(
"How long overlay history lines stay visible; 0 skips them (COMMANDLINEFADETIME)."
))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(24))
.push(text(crate::t!("Ribbon")).size(15))
.push(Space::new().height(10))
@@ -712,6 +969,15 @@ pub fn view_window<'a>(
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(selection.selection_cycling)
.on_toggle(Message::SelectionCyclingChanged)
.size(15),
text(crate::t!("Clicking overlapping objects opens a picker")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(24),
row![
text(crate::t!("Visual Effect Settings")).size(15),
@@ -918,7 +1184,129 @@ pub fn view_window<'a>(
.spacing(0)
.width(sizing.width);
let drawing = column![
// ANNOAUTOSCALE's magnitude decides which objects a newly added scale
// reaches — 1 skips layers that are off, frozen, locked or frozen in the
// viewport, 2 keeps locked ones, 3 skips only locked, 4 takes everything.
// The sign is the on/off the status-bar pill flips, so the mode survives
// being switched off and back on.
let auto_scale_options = [
(0i8, "Off"),
(1, "Skip objects on layers that are off, frozen or locked"),
(2, "Skip objects on layers that are off or frozen"),
(3, "Skip objects on locked layers"),
(4, "All annotative objects"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let selected_auto_scale = auto_scale_options
.iter()
.find(|choice| choice.value == prefs.annotation_auto_scale.max(0))
.cloned();
let qdim_options = [
(0u8, "Endpoints"),
(1, "Intersections"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let selected_qdim = qdim_options
.iter()
.find(|choice| choice.value == prefs.qdim_snap_priority)
.cloned();
let user_prefs = column![
text(crate::t!("User Preferences")).size(15),
Space::new().height(10),
text(crate::t!("Zoom")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.zoom_wheel_reversed)
.on_toggle(Message::ZoomWheelReversedChanged)
.size(15),
text(crate::t!("Reverse mouse wheel zoom (ZOOMWHEEL)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(12),
row![
text(crate::t!("Zoom factor")).size(12).width(150),
slider(3..=100, prefs.zoom_factor.clamp(3, 100), Message::ZoomFactorChanged)
.step(1)
.width(Fill),
text(prefs.zoom_factor.clamp(3, 100).to_string()).size(11).width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!("How far one wheel notch zooms (ZOOMFACTOR)."))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Text and Dimensions")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.texteditmode)
.on_toggle(Message::TextEditModeChanged)
.size(15),
text(crate::t!("TEXTEDIT edits one object and ends (TEXTEDITMODE)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(12),
row![
iced::widget::checkbox(prefs.dimension_continue_mode == 1)
.on_toggle(Message::DimContinueModeChanged)
.size(15),
text(crate::t!(
"Continued dimensions inherit the base dimension's layer and style (DIMCONTINUEMODE)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(12),
row![
text(crate::t!("QDIM origin priority")).size(12).width(150),
iced::widget::pick_list(selected_qdim, qdim_options, |choice| choice.label.clone())
.on_select(|choice| Message::QdimSnapPriorityChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Which points QDIM measures from. Also settable inside the command."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Annotation")).size(15),
Space::new().height(10),
row![
text(crate::t!("Add scales automatically")).size(12).width(150),
iced::widget::pick_list(selected_auto_scale, auto_scale_options, |choice| {
choice.label.clone()
})
.on_select(|choice| Message::AnnoAutoScaleChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Which annotative objects pick up a newly set annotation scale (ANNOAUTOSCALE)."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Block Edit")).size(15),
Space::new().height(10),
row![
@@ -938,6 +1326,336 @@ pub fn view_window<'a>(
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(24),
text(crate::t!("Drawing Units")).size(15),
Space::new().height(10),
row![
text(crate::t!("Length, angle and insertion units"))
.size(12)
.width(Fill),
button(text(crate::t!("Drawing Units…")).size(11))
.on_press(Message::OpenDrawingUnits)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
// Snap modes, grid and object snap stay in the Drafting Settings dialog.
// These two have no home: Drafting Settings shows the rotation angle but
// offers only Reset, and the polar increment lives solely in a status-bar
// pop-up.
let polar_options = [90.0f32, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0, 1.0]
.into_iter()
.map(|value| Labelled {
label: format!("{}°", crate::app::settings::format_snap_angle(value)),
value,
})
.collect::<Vec<_>>();
let selected_polar = polar_options
.iter()
.find(|choice| (choice.value - prefs.polar_increment_deg).abs() < 1e-4)
.cloned()
.unwrap_or_else(|| Labelled {
value: prefs.polar_increment_deg,
label: format!(
"{}°",
crate::app::settings::format_snap_angle(prefs.polar_increment_deg)
),
});
let drafting = column![
text(crate::t!("Drafting")).size(15),
Space::new().height(10),
row![
text(crate::t!("Drafting rotation")).size(12).width(150),
text_input("0", snap_angle_input)
.on_input(Message::SnapAngleInputChanged)
.width(110),
text(crate::t!("degrees")).size(11),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Rotates the crosshair and the snap grid in the active UCS (SNAPANG)."
))
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
text(crate::t!("Polar tracking increment")).size(12).width(150),
iced::widget::pick_list(Some(selected_polar), polar_options, |choice| {
choice.label.clone()
})
.on_select(|choice| Message::PolarIncrementChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(24),
text(crate::t!("Snap and Grid")).size(15),
Space::new().height(10),
row![
text(crate::t!("Snap modes, grid spacing and object snap"))
.size(12)
.width(Fill),
button(text(crate::t!("Drafting Settings…")).size(11))
.on_press(Message::ToggleSnapPopup)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
let surface_type_options = [
(5i16, "Quadratic B-spline"),
(6, "Cubic B-spline"),
(8, "Bezier"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: format!("{} ({value})", crate::t!(label)),
})
.collect::<Vec<_>>();
let selected_surface_type = surface_type_options
.iter()
.find(|choice| choice.value == drawing_prefs.surface_type)
.cloned();
let show_history_options = [
(0i16, "Never"),
(1, "As set per solid"),
(2, "Always"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let selected_show_history = show_history_options
.iter()
.find(|choice| choice.value == drawing_prefs.show_solid_history.clamp(0, 2))
.cloned();
let mut modeling = column![
text(crate::t!("3D Modeling")).size(15),
Space::new().height(10),
text(crate::t!("Display Tools")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.show_viewcube)
.on_toggle(Message::ShowViewCubeChanged)
.size(15),
text(crate::t!("Show the navigation cube (NAVVCUBE)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.show_ucs_icon)
.on_toggle(Message::ShowUcsIconChanged)
.size(15),
text(crate::t!("Show the UCS icon (UCSICON)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.ucs_icon_at_origin)
.on_toggle(Message::UcsIconAtOriginChanged)
.size(15),
text(crate::t!("Place the UCS icon at the origin (UCSICON ORigin)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
];
if drawing_prefs.available {
modeling = modeling
.push(Space::new().height(24))
.push(
row![
text(crate::t!("Display Resolution")).size(15),
Space::new().width(10),
text(crate::t!("applies to the current drawing")).size(11),
]
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Isolines per surface")).size(12).width(150),
// Retessellate once when dragging ends.
slider(
0..=64,
drawing_prefs.isolines.clamp(0, 64),
Message::IsolinesChanged,
)
.step(1i16)
.on_release(Message::IsolinesReleased)
.width(Fill),
text(drawing_prefs.isolines.clamp(0, 64).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
iced::widget::checkbox(drawing_prefs.display_silhouette)
.on_toggle(Message::DispSilhChanged)
.size(15),
text(crate::t!("Show silhouette edges on solids (DISPSILH)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Surface density U")).size(12).width(150),
slider(
0..=200,
drawing_prefs.surface_u.clamp(0, 200),
Message::SurfaceUChanged,
)
.step(1i16)
.width(Fill),
text(drawing_prefs.surface_u.clamp(0, 200).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Surface density V")).size(12).width(150),
slider(
0..=200,
drawing_prefs.surface_v.clamp(0, 200),
Message::SurfaceVChanged,
)
.step(1i16)
.width(Fill),
text(drawing_prefs.surface_v.clamp(0, 200).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Surface type")).size(12).width(150),
iced::widget::pick_list(
selected_surface_type,
surface_type_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::SurfaceTypeChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(24))
.push(
row![
text(crate::t!("Solid History")).size(15),
Space::new().width(10),
text(crate::t!("applies to the current drawing")).size(11),
]
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
iced::widget::checkbox(drawing_prefs.record_solid_history)
.on_toggle(Message::SolidHistChanged)
.size(15),
text(crate::t!("Record the history of composite solids (SOLIDHIST)"))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Show solid history")).size(12).width(150),
iced::widget::pick_list(
selected_show_history,
show_history_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::ShowHistChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
);
}
let modeling = modeling.spacing(0).width(sizing.width);
// The application has no support-file search path, so this page does not
// pretend to offer one. It shows where things actually live and opens the
// folder — which is the question people are really asking when they go
// looking for a Files page.
let folder_row = |label: std::borrow::Cow<'a, str>, path: Option<String>| {
let shown = path.clone().unwrap_or_else(|| crate::t!("Not available").into_owned());
let mut open = button(text(crate::t!("Open folder")).size(11))
.padding([4, 10])
.style(button::secondary);
if let Some(path) = path {
open = open.on_press(Message::OpenFolder(path));
}
row![
column![
text(label).size(12),
text(shown).size(11),
]
.spacing(2)
.width(Fill),
open,
]
.spacing(10)
.align_y(iced::Center)
};
let files = column![
text(crate::t!("Files")).size(15),
Space::new().height(6),
text(crate::t!(
"Where the application keeps its own files. These locations are fixed."
))
.size(11)
.width(sizing.width),
Space::new().height(16),
folder_row(crate::t!("Configuration"), folders.config.clone()),
Space::new().height(12),
folder_row(crate::t!("Plot styles"), folders.plot_styles.clone()),
Space::new().height(12),
folder_row(crate::t!("Plugins"), folders.plugins.clone()),
Space::new().height(12),
folder_row(crate::t!("Autosave files"), folders.autosave.clone()),
]
.spacing(0)
.width(sizing.width);
@@ -946,41 +1664,56 @@ pub fn view_window<'a>(
OptionsTab::General => general.into(),
OptionsTab::Display => display_element.into(),
OptionsTab::Selection => selection.into(),
OptionsTab::Drawing => drawing.into(),
OptionsTab::Files => files.into(),
OptionsTab::OpenAndSave => open_and_save.into(),
OptionsTab::Drafting => drafting.into(),
OptionsTab::Modeling => modeling.into(),
OptionsTab::UserPreferences => user_prefs.into(),
};
// A vertical rail rather than a horizontal strip: the tab names are
// translated into 21 languages, and a row of them stops fitting long
// before the list of pages is complete.
let tab_button = |label, tab| {
let selected = active_tab == tab;
button(text(label).size(12))
button(text(label).size(12.5))
.on_press(Message::OptionsTabChanged(tab))
.padding([6, 14])
.style(if selected { button::primary } else { button::secondary })
.padding([7, 12])
.width(Fill)
.style(if selected { button::primary } else { button::text })
};
let tabs = row![
let tabs = column![
tab_button(crate::t!("General"), OptionsTab::General),
tab_button(crate::t!("Files"), OptionsTab::Files),
tab_button(crate::t!("Open and Save"), OptionsTab::OpenAndSave),
tab_button(crate::t!("Display"), OptionsTab::Display),
tab_button(crate::t!("Drafting"), OptionsTab::Drafting),
tab_button(crate::t!("3D Modeling"), OptionsTab::Modeling),
tab_button(crate::t!("Selection"), OptionsTab::Selection),
tab_button(crate::t!("Drawing"), OptionsTab::Drawing),
tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences),
]
.spacing(6);
.spacing(2)
.width(iced::Length::Fixed(TAB_RAIL_WIDTH));
let body = column![
tabs,
Space::new().height(12),
let pane = column![
// Keep the scrollbar in its own lane instead of floating over the
// controls at the trailing edge of the Options content.
scrollable(content).spacing(8).height(sizing.height),
scrollable(content).spacing(8).height(Fill),
Space::new().height(12),
row![Space::new().width(sizing.width), close],
row![Space::new().width(Fill), close],
]
.width(sizing.width)
.width(Fill)
.height(sizing.height);
let body = row![tabs, Space::new().width(18), pane]
.width(sizing.width)
.height(sizing.height);
let intrinsic = sizing.width == crate::ui::modal::ModalSizing::INTRINSIC.width;
container(body)
.style(container::rounded_box)
.padding([16, 18])
.width(if intrinsic { iced::Length::Fixed(540.0) } else { sizing.width })
.height(if intrinsic { iced::Length::Fixed(560.0) } else { sizing.height })
.width(if intrinsic { iced::Length::Fixed(DIALOG_WIDTH) } else { sizing.width })
.height(if intrinsic { iced::Length::Fixed(DIALOG_HEIGHT) } else { sizing.height })
.into()
}