Add persistent sketch-constraint system (ocs_gcs port) and named-parameters handoff docs

Ports FreeCAD's planegcs 2D geometric constraint solver to Rust as a new
crates/ocs_gcs workspace member, and wires it into the document/scene model
as a persistent SketchConstraintSet: Coincident, Horizontal/Vertical,
Parallel/Perpendicular, Equal, Tangent, Distance, Angle and Radius
constraints that re-solve live (including mid-drag, not just on release),
render glyphs and a DOF badge, flag conflicts with a one-click resolver
pill, and round-trip through save/load via an XRecord blob.

Also includes docs/command_reference.md (a statically generated reference
of every registered command and its prompt sequence) and
docs/named_parameters_design.md + docs/backlog.md, which hand off the next
queued feature (a named-parameter/expression table) and consolidate every
deliberate scope-down and open gap found along the way, so a fresh session
has a complete starting point.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-08 22:19:23 +02:00
1 parent a39c7b8f71
commit 63a74b19a9
63 files changed
+12193 -49

No files matched your search

+47
View File
@@ -645,6 +645,7 @@ pub fn selection_overlay<'a>(
crosshair_bg: [f32; 4],
crosshair: CrosshairOptions,
selection_visual: SelectionVisualOptions,
constraint_glyphs: Vec<(Point, String, bool)>,
) -> Element<'a, Message> {
canvas(SelectionCanvas {
selection,
@@ -668,6 +669,7 @@ pub fn selection_overlay<'a>(
crosshair_bg,
crosshair,
selection_visual,
constraint_glyphs,
})
.width(Length::Fill)
.height(Length::Fill)
@@ -726,6 +728,13 @@ struct SelectionCanvas {
crosshair_bg: [f32; 4],
crosshair: CrosshairOptions,
selection_visual: SelectionVisualOptions,
/// One glyph per persistent sketch constraint in the current scope
/// (design doc §6.3/§7): screen anchor + label text (its symbol, plus a
/// dimensional kind's driving value — `sketch_constraints::glyph_label`)
/// + whether `solve_scope`'s `classify_redundant` call (stage 10/11)
/// flagged this constraint as redundant/conflicting — the design doc
/// §6.3's recommended "glyph color, not wire recoloring" state cue.
constraint_glyphs: Vec<(Point, String, bool)>,
}
fn draw_grip_marker(
@@ -1618,6 +1627,44 @@ impl canvas::Program<Message> for SelectionCanvas {
frame.stroke(&b1, stroke.clone());
frame.stroke(&b2, stroke);
}
// Persistent sketch-constraint glyphs (design doc §6.3/§7): a small
// pill at each constraint's anchor point, its symbol (plus value for
// a dimensional kind) as text — visual-only for now, no hit-testing.
if !self.constraint_glyphs.is_empty() {
const GLYPH_SIZE: f32 = 11.0;
const GLYPH_PAD_X: f32 = 5.0;
const GLYPH_PAD_Y: f32 = 2.0;
let normal_bg = theme.palette().primary.base.color;
let normal_fg = theme.palette().primary.base.text;
// Design doc §6.3's recommended color cue: a constraint
// `solve_scope` flagged as redundant/conflicting (stage 10/11's
// `classify_redundant`) gets the danger palette instead of the
// ordinary primary one — same information a resolver panel
// would show, surfaced right on the geometry.
let conflict_bg = theme.palette().danger.base.color;
let conflict_fg = theme.palette().danger.base.text;
for (anchor, label, is_conflicting) in &self.constraint_glyphs {
if !anchor.x.is_finite() || !anchor.y.is_finite() {
continue;
}
let (bg, fg) = if *is_conflicting { (conflict_bg, conflict_fg) } else { (normal_bg, normal_fg) };
// Rough width estimate (monospace-ish glyph set, short labels)
// avoids a text-measurement pass just to size the background pill.
let w = label.chars().count() as f32 * GLYPH_SIZE * 0.62 + GLYPH_PAD_X * 2.0;
let h = GLYPH_SIZE + GLYPH_PAD_Y * 2.0;
let top_left = Point::new(anchor.x - w * 0.5, anchor.y - h * 0.5);
let pill = canvas::Path::rounded_rectangle(top_left, iced::Size::new(w, h), (h * 0.5).into());
frame.fill(&pill, bg);
frame.fill_text(canvas::Text {
content: label.clone(),
position: Point::new(top_left.x + GLYPH_PAD_X, top_left.y + GLYPH_PAD_Y),
color: fg,
size: iced::Pixels(GLYPH_SIZE),
shaping: iced::advanced::text::Shaping::Advanced,
..Default::default()
});
}
}
// Small cross at each acquired tracking point.
for ost in &self.ost_points {
let tp = ost.screen;
+56 -6
View File
@@ -95,6 +95,12 @@ pub struct Ribbon {
tool_bar_h: Arc<AtomicU32>,
/// User-chosen panel density (persisted). `Auto` sizes by window width.
collapse_mode: CollapseMode,
/// Font size (px) of the caption under a large button's icon (persisted;
/// Options → Display → "Ribbon label size").
label_font_size: i32,
/// Font size (px) of a panel's own title, shown beneath its tools
/// (persisted; Options → Display → "Ribbon section title size").
group_title_font_size: i32,
/// Set by `CollapsePanels` when the tool row is in its tight state; the mode
/// selector hides itself then to give the cramped tab row its space back.
collapse_tight: Arc<AtomicBool>,
@@ -191,6 +197,8 @@ impl Ribbon {
tool_bar_h: Arc::new(AtomicU32::new(TOOL_BAR_H.to_bits())),
collapse_mode: CollapseMode::default(),
collapse_tight: Arc::new(AtomicBool::new(false)),
label_font_size: 10,
group_title_font_size: 9,
}
}
@@ -205,6 +213,28 @@ impl Ribbon {
self.collapse_mode
}
/// Change the icon-label font size. Persistence is handled by the caller
/// via the consolidated app config (`save_config`).
pub fn set_label_font_size(&mut self, size: i32) {
self.label_font_size = size.clamp(8, 16);
}
/// The current icon-label font size (for saving into the app config).
pub fn label_font_size(&self) -> i32 {
self.label_font_size
}
/// Change the panel-title font size. Persistence is handled by the
/// caller via the consolidated app config (`save_config`).
pub fn set_group_title_font_size(&mut self, size: i32) {
self.group_title_font_size = size.clamp(7, 14);
}
/// The current panel-title font size (for saving into the app config).
pub fn group_title_font_size(&self) -> i32 {
self.group_title_font_size
}
/// Current tab-bar height as last measured by the `WrapBar` widget.
fn tab_bar_height(&self) -> f32 {
f32::from_bits(self.tab_bar_h.load(Ordering::Relaxed))
@@ -599,6 +629,8 @@ impl Ribbon {
&self.active_linetype,
self.active_lineweight,
&style_ctx,
self.label_font_size,
self.group_title_font_size,
),
render_group(
true,
@@ -613,6 +645,8 @@ impl Ribbon {
&self.active_linetype,
self.active_lineweight,
&style_ctx,
self.label_font_size,
self.group_title_font_size,
),
collapse_button(
g,
@@ -627,6 +661,7 @@ impl Ribbon {
&self.active_linetype,
self.active_lineweight,
&style_ctx,
self.label_font_size,
false,
),
collapse_button(
@@ -642,13 +677,14 @@ impl Ribbon {
&self.active_linetype,
self.active_lineweight,
&style_ctx,
self.label_font_size,
true,
),
],
}
})
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
CollapsePanels::new(panels, self.collapsed_open.clone(), widgets::tool_bar_h(self.group_title_font_size as f32))
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
@@ -1295,7 +1331,7 @@ impl Ribbon {
}
}
/// Render a single ribbon panel (tools + group label), fixed `TOOL_BAR_H` tall.
/// Render a single ribbon panel (tools + group label), `tool_bar_h(group_title_font_size)` tall.
/// When `compact`, large tools/dropdowns are drawn as small icon columns.
#[allow(clippy::too_many_arguments)]
fn render_group<'a>(
@@ -1311,6 +1347,8 @@ fn render_group<'a>(
active_linetype: &'a str,
active_lineweight: LineWeight,
style_ctx: &StyleContext<'_>,
label_font_size: i32,
group_title_font_size: i32,
) -> Element<'a, Message> {
let mut items_row: Vec<Element<Message>> = Vec::new();
let mut small_buf: Vec<Element<Message>> = Vec::new();
@@ -1327,6 +1365,7 @@ fn render_group<'a>(
active_lineweight,
style_ctx,
compact,
label_font_size: label_font_size as f32,
};
for item in &group.tools {
@@ -1364,12 +1403,17 @@ fn render_group<'a>(
column![
tools_el,
container(text(t!(group.title)).size(9).style(muted_text_style)).padding([1, 4]),
container(
text(t!(group.title))
.size(group_title_font_size as f32)
.style(muted_text_style),
)
.padding([1, 4]),
]
.align_x(iced::Center)
.spacing(0)
.padding([3u16, 4])
.height(Length::Fixed(TOOL_BAR_H))
.height(Length::Fixed(widgets::tool_bar_h(group_title_font_size as f32)))
.into()
}
@@ -1432,6 +1476,7 @@ fn collapse_button<'a>(
active_linetype: &'a str,
active_lineweight: LineWeight,
style_ctx: &StyleContext<'_>,
label_font_size: i32,
// When set, this is the TIGHT step (reached once even the all-collapsed row
// overflows): the whole panel becomes a single non-running button — the first
// tool's icon + a ▾ that opens the tools flyout. Nothing runs directly here.
@@ -1472,7 +1517,9 @@ fn collapse_button<'a>(
.style(button::subtle)
.width(Fill)
.padding([3, 5]);
return automatic_large_button(localized_title, content.into());
// This tight-mode panel *title* (not an icon caption) keeps its own
// fixed size regardless of the icon-label font-size setting.
return automatic_large_button(localized_title, content.into(), 9.0);
}
// Collapsed (not yet tight): a large representative-tool face — a live button
@@ -1495,6 +1542,7 @@ fn collapse_button<'a>(
active_lineweight,
style_ctx,
compact: false,
label_font_size: label_font_size as f32,
},
)
}
@@ -1513,6 +1561,7 @@ fn collapse_button<'a>(
active_lineweight,
style_ctx,
compact: false,
label_font_size: label_font_size as f32,
},
)
}
@@ -1537,7 +1586,8 @@ fn collapse_button<'a>(
.style(button::subtle)
.width(Fill)
.padding([1, 4]);
let opener = automatic_large_button(localized_title, opener.into());
// Same fixed-size opener title as the tight-mode branch above.
let opener = automatic_large_button(localized_title, opener.into(), 9.0);
// The large face fills a fixed slot so a collapsed panel is shorter than a full
// 3-row panel, letting `CollapsePanels` shrink the ribbon row.
+88 -28
View File
@@ -56,15 +56,40 @@ pub(super) const LARGE_W: f32 = ROW_H * 2.2;
const LARGE_LABEL_HPAD: f32 = 8.0;
/// Large ribbon labels use at most two lines before their button grows.
const LARGE_LABEL_LINES: f32 = 2.0;
const LARGE_LABEL_SIZE: f32 = 10.0;
/// Width of a 1-row (small) button.
pub(super) const SMALL_W: f32 = ROW_H;
/// Width of the ▾ strip on a small dropdown.
pub(super) const ARROW_W: f32 = ROW_H * 0.4;
/// Height of the ▾ strip at the bottom of a large dropdown.
pub(super) const LARGE_ARR: f32 = ROW_H * 0.55;
/// Total ribbon tool-area height = 3 × ROW_H + 6 px v-padding + 12 px group-label.
/// Total ribbon tool-area height = 3 × ROW_H + 6 px v-padding + 12 px group-label,
/// at the default 9px group-title font size. Use [`tool_bar_h`] once the live
/// `group_title_font_size` setting is in scope — this constant is only the
/// floor `tool_bar_h` never drops below, kept as its own name for the few
/// call sites that predate the size becoming user-configurable.
pub(super) const TOOL_BAR_H: f32 = 3.0 * ROW_H + 18.0;
/// The group-title label's rendered height (`group_title_font_size == 9`)
/// baked into [`TOOL_BAR_H`]'s fixed 18px allowance (6px padding + this).
const GROUP_TITLE_LABEL_H_AT_DEFAULT: f32 = 12.0;
const GROUP_TITLE_FONT_SIZE_DEFAULT: f32 = 9.0;
/// Ribbon tool-area height for a given group-title font size.
///
/// The panel column that holds a group's icons above its title label is a
/// fixed height (`TOOL_BAR_H`, sized for the *default* title font). Raising
/// the "Section title size" option (`RibbonConfig::group_title_font_size`,
/// `src/app/config.rs`) grows the title text without growing that fixed
/// column, so the icon row above it — which fills whatever space is left —
/// gets squeezed and its icons visibly shrink. Scaling the label's share of
/// the height with the font size (proportionally to the default 12px
/// allowance at the default 9px size) keeps the icon row's share constant
/// instead. Clamped to never go *below* `TOOL_BAR_H`, so the default and
/// smaller settings render pixel-identically to before this existed.
pub(super) fn tool_bar_h(group_title_font_size: f32) -> f32 {
let label_h = GROUP_TITLE_LABEL_H_AT_DEFAULT * (group_title_font_size / GROUP_TITLE_FONT_SIZE_DEFAULT);
(3.0 * ROW_H + 6.0 + label_h).max(TOOL_BAR_H)
}
/// Height of a collapsed panel button's large representative face (big icon +
/// its label). A collapsed button is this face plus the title opener, so it is
/// shorter than a full 3-row panel — the ribbon height follows it down.
@@ -86,8 +111,11 @@ const COMBO_PANEL_PAD: Padding = Padding {
thread_local! {
/// Ribbon layout runs on every pointer-driven view update. Cache the font-
/// measured width per translated label so the automatic sizing stays cheap.
static LARGE_WIDTH_CACHE: RefCell<HashMap<String, f32>> =
/// measured width per (translated label, font size) so the automatic
/// sizing stays cheap — keyed on both since the same label text is
/// measured at more than one size (a full-density button's own caption
/// vs. a collapsed panel's smaller opener title).
static LARGE_WIDTH_CACHE: RefCell<HashMap<(String, u32), f32>> =
RefCell::new(HashMap::default());
}
@@ -96,6 +124,7 @@ fn ribbon_label_bounds(
label: &str,
width: f32,
wrapping: advanced_text::Wrapping,
label_size: f32,
) -> Size {
use advanced_text::{Paragraph as _, Renderer as _};
@@ -103,7 +132,7 @@ fn ribbon_label_bounds(
advanced_text::Text {
content: label,
bounds: Size::new(width, f32::INFINITY),
size: Pixels(LARGE_LABEL_SIZE),
size: Pixels(label_size),
line_height: advanced_text::LineHeight::default(),
font: renderer.default_font(),
align_x: advanced_text::Alignment::Center,
@@ -117,28 +146,37 @@ fn ribbon_label_bounds(
paragraph.min_bounds()
}
/// Measure the translated label at the normal button width. It wraps first;
/// only labels that would need more than two lines widen their button. The
/// binary search uses the renderer's real font metrics, so locale and UI scale
/// Widest a large button will grow to keep its label on one line before
/// falling back to two-line wrapping instead. A label pre-broken with an
/// explicit `\n` (long labels get hand-wrapped at the source, e.g. "Viewport
/// Configuration") measures its own natural per-line width regardless, so
/// this only ever engages for a plain label with no manual break.
const LARGE_LABEL_MAX_SINGLE_LINE_W: f32 = LARGE_W * 2.0;
/// Measure the translated label at the normal button width. Prefers widening
/// the button to keep the label on one line — most labels are short enough
/// for this to be a small, one-time bump — falling back to two-line wrapping
/// only once even a single line would need excessive width. The binary
/// search uses the renderer's real font metrics, so locale and UI scale
/// changes do not rely on character-count estimates.
fn measure_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
fn measure_large_width(renderer: &iced::Renderer, label: &str, label_size: f32) -> f32 {
let base_inner = (LARGE_W - LARGE_LABEL_HPAD).max(1.0);
let line_height = advanced_text::LineHeight::default()
.to_absolute(Pixels(LARGE_LABEL_SIZE))
.to_absolute(Pixels(label_size))
.0;
let max_label_height = line_height * LARGE_LABEL_LINES + 0.5;
let fits = |width: f32| {
let fits_one_line = |width: f32| {
ribbon_label_bounds(
renderer,
label,
width,
advanced_text::Wrapping::WordOrGlyph,
label_size,
)
.height
<= max_label_height
<= line_height + 0.5
};
if fits(base_inner) {
if fits_one_line(base_inner) {
return LARGE_W;
}
@@ -147,18 +185,34 @@ fn measure_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
label,
f32::INFINITY,
advanced_text::Wrapping::None,
label_size,
)
.width
.max(base_inner);
if !fits(natural) {
if natural <= LARGE_LABEL_MAX_SINGLE_LINE_W {
return (natural + LARGE_LABEL_HPAD).ceil();
}
// Long enough that a single line would make the button unreasonably
// wide — wrap to two lines instead, at the narrowest width (up to the
// same cap) that fits.
let max_label_height = line_height * LARGE_LABEL_LINES + 0.5;
let fits_two_lines = |width: f32| {
ribbon_label_bounds(
renderer,
label,
width,
advanced_text::Wrapping::WordOrGlyph,
label_size,
)
.height
<= max_label_height
};
let mut low = base_inner;
let mut high = natural;
let mut high = LARGE_LABEL_MAX_SINGLE_LINE_W;
for _ in 0..10 {
let mid = (low + high) * 0.5;
if fits(mid) {
if fits_two_lines(mid) {
high = mid;
} else {
low = mid;
@@ -167,14 +221,15 @@ fn measure_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
(high + LARGE_LABEL_HPAD).ceil().max(LARGE_W)
}
fn automatic_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
if let Some(width) = LARGE_WIDTH_CACHE.with(|cache| cache.borrow().get(label).copied()) {
fn automatic_large_width(renderer: &iced::Renderer, label: &str, label_size: f32) -> f32 {
let key = (label.to_string(), label_size.to_bits());
if let Some(width) = LARGE_WIDTH_CACHE.with(|cache| cache.borrow().get(&key).copied()) {
return width;
}
let width = measure_large_width(renderer, label);
let width = measure_large_width(renderer, label, label_size);
LARGE_WIDTH_CACHE.with(|cache| {
cache.borrow_mut().insert(label.to_string(), width);
cache.borrow_mut().insert(key, width);
});
width
}
@@ -182,6 +237,7 @@ fn automatic_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
struct AutomaticLargeWidth<'a> {
label: String,
content: Element<'a, Message>,
label_size: f32,
}
impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
@@ -207,7 +263,7 @@ impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
renderer: &iced::Renderer,
limits: &layout::Limits,
) -> layout::Node {
let width = automatic_large_width(renderer, &self.label);
let width = automatic_large_width(renderer, &self.label, self.label_size);
self.content.as_widget_mut().layout(
tree,
renderer,
@@ -287,8 +343,9 @@ impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
pub(super) fn automatic_large_button<'a>(
label: String,
content: Element<'a, Message>,
label_size: f32,
) -> Element<'a, Message> {
Element::new(AutomaticLargeWidth { label, content })
Element::new(AutomaticLargeWidth { label, content, label_size })
}
// ── Tab-bar constants ──────────────────────────────────────────────────────
@@ -633,6 +690,9 @@ pub(super) struct RenderCtx<'a> {
pub style_ctx: &'a StyleContext<'a>,
/// When compact, the Properties panel's Match button shrinks to a small icon.
pub compact: bool,
/// Font size (px) of the caption under a large button's icon (Options →
/// Display → "Ribbon label size").
pub label_font_size: f32,
}
/// A large dropdown button: the current icon on top, its ▾ directly beneath the
@@ -682,7 +742,7 @@ pub(super) fn render_large_dropdown<'a>(
.align_x(iced::Center)
.align_y(iced::Center),
text(label.clone())
.size(10)
.size(ctx.label_font_size)
.width(Fill)
.align_x(iced::Center)
.wrapping(advanced_text::Wrapping::WordOrGlyph),
@@ -735,7 +795,7 @@ pub(super) fn render_large_dropdown<'a>(
.width(Fill)
.height(Fill);
PosReport::new(id, automatic_large_button(label, content.into()))
PosReport::new(id, automatic_large_button(label, content.into(), ctx.label_font_size))
.into()
}
@@ -815,7 +875,7 @@ pub(super) fn render_large<'a>(
.align_x(iced::Center)
.align_y(iced::Center),
text(label.clone())
.size(10)
.size(ctx.label_font_size)
.width(Fill)
.align_x(iced::Center)
.wrapping(advanced_text::Wrapping::WordOrGlyph),
@@ -836,7 +896,7 @@ pub(super) fn render_large<'a>(
left: 4.0,
});
tooltip(
automatic_large_button(label, btn.into()),
automatic_large_button(label, btn.into(), ctx.label_font_size),
make_tip(tip_text),
TipPos::Right,
)
+67
View File
@@ -125,6 +125,16 @@ impl StatusBar {
// Which pills the user has chosen to show on the bar.
config: &'a StatusBarConfig,
menu_data: StatusMenuData<'a>,
// Remaining degrees of freedom for the current sketch scope's
// persistent constraints (design doc §6.3) — `None` when the scope
// has no `SketchConstraintSet` yet (nothing constrained), so the
// badge stays invisible until it's actually relevant.
sketch_dof: Option<usize>,
// Number of redundant/conflicting constraints `solve_scope` flagged
// in the current sketch scope (design doc §6.4, stage 11) — 0 hides
// the pill entirely, so an ordinarily/fully-constrained drawing sees
// no new clutter.
sketch_conflicts: usize,
) -> Element<'a, Message> {
let StatusMenuData {
layout_names,
@@ -243,6 +253,44 @@ impl StatusBar {
.into(),
);
}
// Not a `StatusPill` (so not user-hideable yet): invisible until the
// current scope actually has a SketchConstraintSet, so a drawing
// that never uses parametric constraints sees no new clutter.
if let Some(dof) = sketch_dof {
let pill = if dof == 0 {
success_pill(crate::tf!("DOF: {dof}").into_owned())
} else {
status_pill(crate::tf!("DOF: {dof}").into_owned())
};
pills.push(
tip(
pill,
if dof == 0 {
t!("Fully constrained — no remaining degrees of freedom")
} else {
t!("Remaining degrees of freedom in the current sketch's persistent constraints")
},
)
.into(),
);
}
// Design doc §6.4 (stage 11): a bounded v1 of the guided conflict
// resolver — click removes one flagged constraint at a time. See
// `OpenCADStudio::resolve_one_sketch_conflict`'s doc comment for the
// full scope-down rationale versus the design doc's named-candidate,
// cyclable-preview panel.
if sketch_conflicts > 0 {
pills.push(
tip(
action_pill(
crate::tf!("⚠ {sketch_conflicts} conflicting").into_owned(),
Message::ResolveOneSketchConflict,
),
t!("One or more constraints in this sketch conflict or are redundant\nClick to remove one and re-solve"),
)
.into(),
);
}
if vis(StatusPill::Ortho) {
pills.push(
tip(
@@ -1034,6 +1082,25 @@ fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
.into()
}
/// Like [`status_pill`], but with the theme's `success` accent — design doc
/// §6.3's "simple color-coded constraint state" cue: used for the DOF badge
/// when a sketch scope has reached zero remaining degrees of freedom, so
/// "fully constrained" is visible at a glance without opening anything.
fn success_pill(label: impl Into<String>) -> Element<'static, Message> {
container(text(label.into()).size(12))
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.success.weak.color)),
text_color: Some(palette.success.weak.text),
border: Border { color: palette.success.base.color, width: 1.0, radius: 4.0.into() },
..Default::default()
}
})
.padding([4, 8])
.into()
}
// ── Scale popup button ────────────────────────────────────────────────────
/// Shared visual root for labelled status-bar menus (units, scale, …).
+52
View File
@@ -41,6 +41,8 @@ pub fn view_window<'a>(
crosshair_color: Option<[u8; 3]>,
crosshair_color_input: &'a str,
lineweight_display_scale: i32,
ribbon_label_font_size: i32,
ribbon_group_title_font_size: i32,
model_space: &'a crate::app::config::ModelSpaceThemeConfig,
model_bg_input: &'a str,
paper_bg_input: &'a str,
@@ -535,6 +537,56 @@ pub fn view_window<'a>(
text(crate::t!("Changes the on-screen width in Model without affecting plotted output."))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(24))
.push(text(crate::t!("Ribbon")).size(15))
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Icon label size")).size(12).width(140),
slider(
8..=16,
ribbon_label_font_size.clamp(8, 16),
Message::RibbonLabelFontSizeChanged,
)
.step(1)
.width(Fill),
text(format!("{}px", ribbon_label_font_size.clamp(8, 16)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!("Font size of the caption under each ribbon button's icon."))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Section title size")).size(12).width(140),
slider(
7..=14,
ribbon_group_title_font_size.clamp(7, 14),
Message::RibbonGroupTitleFontSizeChanged,
)
.step(1)
.width(Fill),
text(format!("{}px", ribbon_group_title_font_size.clamp(7, 14)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!("Font size of each panel's own title (Draw, Modify, Layers, …)."))
.size(11)
.width(sizing.width),
);
let display_element = display.spacing(0).width(sizing.width);