Harden parametric sketch constraints

This commit is contained in:
Hakan Seven committed 2026-09-11 09:45:57 +03:00
1 parent 41c07e9858
commit 9b58ca8a93
101 files changed
+3128 -13767

No files matched your search

+28 -92
View File
@@ -56,40 +56,15 @@ 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,
/// 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.
/// Total ribbon tool-area height = 3 × ROW_H + 6 px v-padding + 12 px group-label.
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.
@@ -111,11 +86,8 @@ 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, 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>> =
/// measured width per translated label so the automatic sizing stays cheap.
static LARGE_WIDTH_CACHE: RefCell<HashMap<String, f32>> =
RefCell::new(HashMap::default());
}
@@ -124,7 +96,6 @@ fn ribbon_label_bounds(
label: &str,
width: f32,
wrapping: advanced_text::Wrapping,
label_size: f32,
) -> Size {
use advanced_text::{Paragraph as _, Renderer as _};
@@ -132,7 +103,7 @@ fn ribbon_label_bounds(
advanced_text::Text {
content: label,
bounds: Size::new(width, f32::INFINITY),
size: Pixels(label_size),
size: Pixels(LARGE_LABEL_SIZE),
line_height: advanced_text::LineHeight::default(),
font: renderer.default_font(),
align_x: advanced_text::Alignment::Center,
@@ -146,41 +117,27 @@ fn ribbon_label_bounds(
paragraph.min_bounds()
}
/// 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
/// 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
/// changes do not rely on character-count estimates.
fn measure_large_width(renderer: &iced::Renderer, label: &str, label_size: f32) -> f32 {
fn measure_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
let base_inner = (LARGE_W - LARGE_LABEL_HPAD).max(1.0);
let line_height = advanced_text::LineHeight::default()
.to_absolute(Pixels(label_size))
.to_absolute(Pixels(LARGE_LABEL_SIZE))
.0;
// `Wrapping::Word` (not `WordOrGlyph`): a label that doesn't fit at this
// width wraps at the next word boundary instead of splitting mid-word.
// The width check catches the rare case Word-wrapping can't help — a
// single unbreakable "word" wider than the box — so that case correctly
// falls through to the wider stage below instead of reporting a false fit.
let fits_one_line = |width: f32| {
let max_label_height = line_height * LARGE_LABEL_LINES + 0.5;
let fits = |width: f32| {
let bounds = ribbon_label_bounds(
renderer,
label,
width,
advanced_text::Wrapping::Word,
label_size,
);
bounds.width <= width + 0.5 && bounds.height <= line_height + 0.5
bounds.width <= width + 0.5 && bounds.height <= max_label_height
};
if fits_one_line(base_inner) {
if fits(base_inner) {
return LARGE_W;
}
@@ -189,33 +146,18 @@ fn measure_large_width(renderer: &iced::Renderer, label: &str, label_size: f32)
label,
f32::INFINITY,
advanced_text::Wrapping::None,
label_size,
)
.width
.max(base_inner);
if natural <= LARGE_LABEL_MAX_SINGLE_LINE_W {
if !fits(natural) {
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| {
let bounds = ribbon_label_bounds(
renderer,
label,
width,
advanced_text::Wrapping::Word,
label_size,
);
bounds.width <= width + 0.5 && bounds.height <= max_label_height
};
let mut low = base_inner;
let mut high = LARGE_LABEL_MAX_SINGLE_LINE_W;
let mut high = natural;
for _ in 0..10 {
let mid = (low + high) * 0.5;
if fits_two_lines(mid) {
if fits(mid) {
high = mid;
} else {
low = mid;
@@ -224,15 +166,14 @@ fn measure_large_width(renderer: &iced::Renderer, label: &str, label_size: f32)
(high + LARGE_LABEL_HPAD).ceil().max(LARGE_W)
}
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()) {
fn automatic_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
if let Some(width) = LARGE_WIDTH_CACHE.with(|cache| cache.borrow().get(label).copied()) {
return width;
}
let width = measure_large_width(renderer, label, label_size);
let width = measure_large_width(renderer, label);
LARGE_WIDTH_CACHE.with(|cache| {
cache.borrow_mut().insert(key, width);
cache.borrow_mut().insert(label.to_string(), width);
});
width
}
@@ -240,7 +181,6 @@ fn automatic_large_width(renderer: &iced::Renderer, label: &str, label_size: f32
struct AutomaticLargeWidth<'a> {
label: String,
content: Element<'a, Message>,
label_size: f32,
}
impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
@@ -266,7 +206,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, self.label_size);
let width = automatic_large_width(renderer, &self.label);
self.content.as_widget_mut().layout(
tree,
renderer,
@@ -346,9 +286,8 @@ 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, label_size })
Element::new(AutomaticLargeWidth { label, content })
}
// ── Tab-bar constants ──────────────────────────────────────────────────────
@@ -693,9 +632,6 @@ 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
@@ -745,7 +681,7 @@ pub(super) fn render_large_dropdown<'a>(
.align_x(iced::Center)
.align_y(iced::Center),
text(label.clone())
.size(ctx.label_font_size)
.size(10)
.width(Fill)
.align_x(iced::Center)
.wrapping(advanced_text::Wrapping::Word),
@@ -798,7 +734,7 @@ pub(super) fn render_large_dropdown<'a>(
.width(Fill)
.height(Fill);
PosReport::new(id, automatic_large_button(label, content.into(), ctx.label_font_size))
PosReport::new(id, automatic_large_button(label, content.into()))
.into()
}
@@ -878,7 +814,7 @@ pub(super) fn render_large<'a>(
.align_x(iced::Center)
.align_y(iced::Center),
text(label.clone())
.size(ctx.label_font_size)
.size(10)
.width(Fill)
.align_x(iced::Center)
.wrapping(advanced_text::Wrapping::Word),
@@ -899,7 +835,7 @@ pub(super) fn render_large<'a>(
left: 4.0,
});
tooltip(
automatic_large_button(label, btn.into(), ctx.label_font_size),
automatic_large_button(label, btn.into()),
make_tip(tip_text),
TipPos::Right,
)