forked from 1b-agent/opencadstudio-upstream
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:
1 parent
a39c7b8f71
commit
63a74b19a9
63 files changed
+12193
-49
No files matched your search
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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, …).
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user