#885: every checkbox keeps a visible box at rest

The theme leaves egui's own checkbox without an outline while inactive,
so an unchecked box was invisible and a checked one a bare tick. #1080
moved Preferences to widgets::check; the remaining ui.checkbox uses
follow: the Properties panel's Snap to Point, Snap to Grid and Scale
Strokes & Effects, Revolve's Keep visible surfaces and Shading, Save
options, Export for Screens, the Variables dialog and plug-in forms.

widgets::check_name shows a document's name (an artboard's, an asset's)
as it is, never translated; the check and radio rows translate their
labels in the public functions, so choice_row draws text as given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-11 07:56:27 +07:00
parent 389f75fa7b
commit c4e139af19
8 changed files with 88 additions and 48 deletions

File diff suppressed because one or more lines are too long

View File

@ -353,8 +353,10 @@ fn artboards(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE);
}
}
if let Some(b) = boards.get_mut(i) {
ui.checkbox(b, name);
if let Some(b) = boards.get_mut(i)
&& crate::widgets::check_name(ui, name, *b)
{
*b ^= true;
}
});
}
@ -399,10 +401,10 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
ui.horizontal(|ui| {
let (r, _) = ui.allocate_exact_size(egui::vec2(46.0, 46.0), egui::Sense::hover());
crate::panels::asset_export::paint_thumb(ui, st, a, r);
let mut checked = on.contains(&a.id);
if ui.checkbox(&mut checked, a.name.as_str()).changed() {
let checked = on.contains(&a.id);
if crate::widgets::check_name(ui, &a.name, checked) {
on.retain(|id| *id != a.id);
if checked {
if !checked {
on.push(a.id);
}
}

View File

@ -320,8 +320,8 @@ pub(super) fn schema_fields(ui: &mut egui::Ui, d: &mut Dialog, specs: &[(String,
crate::widgets::range_field(ui, ("plugin-int", k), x, *min as f64..=*max as f64, "", 0, 100.0).map(|x| json!(x as i64))
}
ParamSpec::Bool { .. } => {
let mut b = cur.as_bool().unwrap_or(false);
ui.checkbox(&mut b, "").changed().then(|| json!(b))
let b = cur.as_bool().unwrap_or(false);
crate::widgets::check(ui, "", b, true).then(|| json!(!b))
}
ParamSpec::Choice { options, .. } => {
let labels: Vec<&str> = options.iter().map(String::as_str).collect();

View File

@ -93,14 +93,15 @@ fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
super::form::check(ui, d, "__showAxis", tl!("Show axis"));
ui.add_space(6.0);
ui.horizontal(|ui| {
let mut visible = d.fields.get("expandVisibleOnly").and_then(Value::as_bool).unwrap_or(true);
let response = ui.allocate_ui_with_layout(
let visible = d.fields.get("expandVisibleOnly").and_then(Value::as_bool).unwrap_or(true);
let tip = tl!("Trim covered geometry on Expand Appearance. Live preview keeps the full surface. Transparent paints and surfaces exceeding visibility limits retain all faces.");
let toggled = ui.allocate_ui_with_layout(
vec2((ui.available_width() - 30.0).max(100.0), 24.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.checkbox(&mut visible, egui::RichText::new(tl!("Keep visible surfaces only when expanding")).size(12.0))
widgets::check_tip(ui, tl!("Keep visible surfaces only when expanding"), visible, true, tip)
},
).inner;
if response.on_hover_text(tl!("Trim covered geometry on Expand Appearance. Live preview keeps the full surface. Transparent paints and surfaces exceeding visibility limits retain all faces.")).changed() {
d.fields.insert("expandVisibleOnly".into(), json!(visible));
if toggled {
d.fields.insert("expandVisibleOnly".into(), json!(!visible));
}
if reset_dot(ui, "expandVisibleOnly", !visible, "true") {
d.fields.insert("expandVisibleOnly".into(), json!(true));
@ -111,9 +112,9 @@ fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
fn lighting_controls(ui: &mut egui::Ui, d: &mut Dialog) {
ui.horizontal(|ui| {
let mut shade = d.bool("shade");
if ui.checkbox(&mut shade, tl!("Shading")).changed() {
d.fields.insert("shade".into(), json!(shade));
let shade = d.bool("shade");
if widgets::check(ui, tl!("Shading"), shade, true) {
d.fields.insert("shade".into(), json!(!shade));
}
ui.add_space((ui.available_width() - 24.0).max(0.0));
if reset_dot(ui, "shade", !shade, "true") {

View File

@ -145,9 +145,9 @@ fn option_row(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, o: &Forma
let value = d.fields.get(o.name).cloned().unwrap_or(Value::Null);
if o.ty == "boolean" {
ui.label("");
let mut on = value.as_bool().unwrap_or(false);
if ui.checkbox(&mut on, label.trim_end_matches(':')).on_hover_text(o.description).changed() {
d.fields.insert(o.name.into(), json!(on));
let on = value.as_bool().unwrap_or(false);
if widgets::check_tip(ui, label.trim_end_matches(':'), on, true, o.description) {
d.fields.insert(o.name.into(), json!(!on));
}
ui.end_row();
if o.name == "separateArtboards" {

View File

@ -202,9 +202,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
label(ui, &v.name);
let key = value_key(&v.name);
match v.kind {
VariableKind::Text => text(ui, d, &key, 220.0),
VariableKind::Text => {
text(ui, d, &key, 220.0);
}
VariableKind::Visibility => checkbox(ui, d, &key),
};
}
ui.end_row();
}
}
@ -224,13 +226,11 @@ fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> egui::Respo
}
/// A checkbox for `key`, written back as it is toggled.
fn checkbox(ui: &mut egui::Ui, d: &mut Dialog, key: &str) -> egui::Response {
let mut b = d.bool(key);
let r = ui.checkbox(&mut b, "");
if r.changed() {
d.fields.insert(key.into(), json!(b));
fn checkbox(ui: &mut egui::Ui, d: &mut Dialog, key: &str) {
let b = d.bool(key);
if widgets::check(ui, "", b, true) {
d.fields.insert(key.into(), json!(!b));
}
r
}
/// A dropdown over the document's names `options` (shown as they are, never translated) for

View File

@ -356,13 +356,11 @@ fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
});
divider(ui);
section_header(ui, tl!("Snap Options"));
let mut sp = app.ui.view.snap_to_point;
if ui.checkbox(&mut sp, tl!("Snap to Point")).changed() {
app.ui.view.snap_to_point = sp;
if widgets::check(ui, tl!("Snap to Point"), app.ui.view.snap_to_point, true) {
app.ui.view.snap_to_point ^= true;
}
let mut sg = app.ui.view.snap_to_grid;
if ui.checkbox(&mut sg, tl!("Snap to Grid")).changed() {
app.ui.view.snap_to_grid = sg;
if widgets::check(ui, tl!("Snap to Grid"), app.ui.view.snap_to_grid, true) {
app.ui.view.snap_to_grid ^= true;
}
divider(ui);
section_header(ui, tl!("Preferences"));
@ -373,9 +371,9 @@ fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
app.session.prefs.keyboard_increment = v.max(0.001);
}
});
let mut ss = app.session.prefs.scale_strokes;
if ui.checkbox(&mut ss, tl!("Scale Strokes & Effects")).changed() {
super::transform::set_pref(app, "scaleStrokes", ss);
let ss = app.session.prefs.scale_strokes;
if widgets::check(ui, tl!("Scale Strokes & Effects"), ss, true) {
super::transform::set_pref(app, "scaleStrokes", !ss);
}
divider(ui);
section_header(ui, tl!("Quick Actions"));
@ -584,6 +582,40 @@ mod tests {
frame(app, ctx, vec![b(false)]);
}
/// The Snap Options and Scale Strokes & Effects checkboxes keep a visible box at rest in every
/// theme, as the Preferences ones do (#1080), and toggle when clicked.
#[test]
fn the_document_checkboxes_show_their_boxes_and_toggle() {
for brightness in crate::theme::Brightness::ALL {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
crate::theme::apply(&ctx, brightness);
frame(&mut app, &ctx, vec![]);
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(&mut app, ui));
out.textures_delta.clear();
let boxes = out
.shapes
.iter()
.filter(|s| matches!(&s.shape, egui::Shape::Rect(r) if r.stroke.width > 0.0 && (r.rect.width() - 13.0).abs() < 0.5 && (r.rect.height() - 13.0).abs() < 0.5))
.count();
assert!(boxes >= 3, "{brightness:?}: {boxes} checkbox outlines");
}
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let texts = frame(&mut app, &ctx, vec![]);
let (grid, strokes) = (app.ui.view.snap_to_grid, app.session.prefs.scale_strokes);
click(&mut app, &ctx, &texts, "Snap to Grid");
assert_eq!(app.ui.view.snap_to_grid, !grid);
let texts = frame(&mut app, &ctx, vec![]);
click(&mut app, &ctx, &texts, "Scale Strokes & Effects");
assert_eq!(app.session.prefs.scale_strokes, !strokes);
}
/// #921: with the Gradient tool, a Gradient section: the type, and for a freeform gradient
/// Draw as Points or Lines radio buttons.
#[test]

View File

@ -982,12 +982,11 @@ pub fn link_label(ui: &mut Ui, text: &str) -> Response {
resp
}
/// The row of a checkbox or radio button: allocates a 13 pt box plus `label`, draws the label and
/// returns the box, the response and the box's border colour.
fn choice_row(ui: &mut Ui, label: &str, enabled: bool) -> (Rect, Response, Color32) {
/// The row of a checkbox or radio button: allocates a 13 pt box plus `text` (already translated),
/// draws the text and returns the box, the response and the box's border colour.
fn choice_row(ui: &mut Ui, text: &str, enabled: bool) -> (Rect, Response, Color32) {
let t = Tokens::get(ui.ctx());
let galley =
ui.painter().layout_no_wrap(tl!(label).to_string(), egui::FontId::proportional(12.5), if enabled { t.text } else { t.text_disabled });
let galley = ui.painter().layout_no_wrap(text.to_string(), egui::FontId::proportional(12.5), if enabled { t.text } else { t.text_disabled });
let (rect, resp) =
ui.allocate_exact_size(vec2(18.0 + galley.size().x, 20.0f32.max(galley.size().y)), if enabled { Sense::click() } else { Sense::hover() });
let bx = Rect::from_min_size(pos2(rect.left(), rect.center().y - 6.5), Vec2::splat(13.0));
@ -1009,21 +1008,27 @@ pub fn check(ui: &mut Ui, label: &str, value: bool, enabled: bool) -> bool {
/// [`check`] with tooltip `tip` on the box and its label. Returns true when toggled.
pub fn check_tip(ui: &mut Ui, label: &str, value: bool, enabled: bool, tip: &str) -> bool {
let (clicked, resp) = check_row(ui, label, Some(value), enabled);
let (clicked, resp) = check_row(ui, tl!(label), Some(value), enabled);
resp.on_hover_text(tip);
clicked
}
/// Three-state [`check`]: `None` shows a dash (a neutral or mixed state). Returns true when clicked.
pub fn check3(ui: &mut Ui, label: &str, value: Option<bool>, enabled: bool) -> bool {
check_row(ui, label, value, enabled).0
check_row(ui, tl!(label), value, enabled).0
}
/// [`check3`], also returning the row's response (what a tooltip goes on: a widget around it never
/// counts as hovered under the box).
fn check_row(ui: &mut Ui, label: &str, value: Option<bool>, enabled: bool) -> (bool, Response) {
/// [`check`] for a name of the document's (an artboard's, an asset's), shown as it is, never
/// translated. Returns true when toggled.
pub fn check_name(ui: &mut Ui, name: &str, value: bool) -> bool {
check_row(ui, name, Some(value), true).0
}
/// [`check3`] with its `text` already translated, also returning the row's response (what a
/// tooltip goes on: a widget around it never counts as hovered under the box).
fn check_row(ui: &mut Ui, text: &str, value: Option<bool>, enabled: bool) -> (bool, Response) {
let t = Tokens::get(ui.ctx());
let (bx, resp, border) = choice_row(ui, label, enabled);
let (bx, resp, border) = choice_row(ui, text, enabled);
let checked = value == Some(true);
ui.painter().rect_filled(bx, CornerRadius::same(2), if checked && enabled { t.accent_strong } else { t.input });
ui.painter().rect_stroke(bx, CornerRadius::same(2), Stroke::new(1.0, border), StrokeKind::Inside);
@ -1045,7 +1050,7 @@ fn check_row(ui: &mut Ui, label: &str, value: Option<bool>, enabled: bool) -> (b
/// Radio button in the style of [`check`], with a disabled state. Returns true when clicked.
pub fn radio(ui: &mut Ui, label: &str, selected: bool, enabled: bool) -> bool {
let t = Tokens::get(ui.ctx());
let (bx, resp, border) = choice_row(ui, label, enabled);
let (bx, resp, border) = choice_row(ui, tl!(label), enabled);
let c = bx.center();
ui.painter().circle(c, 6.0, if selected && enabled { t.accent_strong } else { t.input }, Stroke::new(1.0, border));
if selected {