From d859c673c1e93bccaa6536fa3653b902ea877cea Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Mon, 26 Oct 2026 16:01:36 -0400 Subject: [PATCH] Sonar: reduce cognitive complexity in highlight, review and outline commands --- crates/doc/src/math_linear.rs | 2 +- crates/docbin/src/officeart.rs | 18 ++-- crates/docx/src/read/story.rs | 7 +- crates/engine/src/cmd/connectors.rs | 119 ++++++++++++++++----------- crates/engine/src/cmd/format.rs | 60 ++++++++------ crates/engine/src/cmd/outline.rs | 86 ++++++++++--------- crates/engine/src/cmd/review.rs | 77 ++++++++--------- crates/engine/src/cmd/text.rs | 2 +- crates/fonts/src/tests_sysfonts.rs | 2 +- crates/layout/src/lib.rs | 2 +- crates/layout/src/tests_bidi.rs | 1 + crates/layout/src/tests_grid.rs | 1 + crates/layout/src/tests_textbox.rs | 1 + crates/proof/src/affix.rs | 11 ++- crates/ui-egui/src/dialogs_layout.rs | 2 +- crates/ui-egui/src/keytips.rs | 2 +- crates/ui-egui/src/objects.rs | 2 +- xtask/src/visual_diff.rs | 2 +- 18 files changed, 216 insertions(+), 181 deletions(-) diff --git a/crates/doc/src/math_linear.rs b/crates/doc/src/math_linear.rs index 3f077f377..39a25ab5d 100644 --- a/crates/doc/src/math_linear.rs +++ b/crates/doc/src/math_linear.rs @@ -570,7 +570,7 @@ impl P { } out.pop() }; - if !matches!(c, '^' | '_') && !(matches!(c, '┬' | '┴') && self.no_limits == 0) && !is_combining(c) { + if !(matches!(c, '^' | '_') || matches!(c, '┬' | '┴') && self.no_limits == 0 || is_combining(c)) { break; } // The base is already deep: the operator is text (see `too_deep`). diff --git a/crates/docbin/src/officeart.rs b/crates/docbin/src/officeart.rs index 6d35d929d..fd665874d 100644 --- a/crates/docbin/src/officeart.rs +++ b/crates/docbin/src/officeart.rs @@ -377,21 +377,15 @@ impl Props { } 0x0104 => self.pib = Some(op), 0x0181 => self.fill_color = color(op), - 0x01BF => { - // fFilled (bit 4) is valid when fUsefFilled (bit 20) is set. - if op & (1 << 20) != 0 { - self.filled = Some(op & (1 << 4) != 0); - } - } + // fFilled (bit 4) is valid when fUsefFilled (bit 20) is set. + 0x01BF if op & (1 << 20) != 0 => self.filled = Some(op & (1 << 4) != 0), + 0x01BF => {} 0x01C0 => self.line_color = color(op), 0x01CB => self.line_width = Some(op), 0x01D0 | 0x01D1 => self.arrowhead |= op != 0, - 0x01FF => { - // fLine (bit 3) is valid when fUsefLine (bit 19) is set. - if op & (1 << 19) != 0 { - self.line = Some(op & (1 << 3) != 0); - } - } + // fLine (bit 3) is valid when fUsefLine (bit 19) is set. + 0x01FF if op & (1 << 19) != 0 => self.line = Some(op & (1 << 3) != 0), + 0x01FF => {} 0x0384..=0x0387 => self.wrap_dist[(pid - 0x0384) as usize] = Some(op), 0x038F => self.posh = Some(op), 0x0390 => self.posrelh = Some(op), diff --git a/crates/docx/src/read/story.rs b/crates/docx/src/read/story.rs index 40b9b7418..73c93cae7 100644 --- a/crates/docx/src/read/story.rs +++ b/crates/docx/src/read/story.rs @@ -732,11 +732,10 @@ impl Reader<'_> { }; self.emit_text(sc, pb, c, props); } - "w:cr" => { - if !sc.fields.iter().any(|f| f.phase == Phase::Instr) { - self.emit_text(sc, pb, "\n", props); - } + "w:cr" if !sc.fields.iter().any(|f| f.phase == Phase::Instr) => { + self.emit_text(sc, pb, "\n", props); } + "w:cr" => {} "w:noBreakHyphen" => self.emit_text(sc, pb, "\u{2011}", props), "w:softHyphen" => self.emit_text(sc, pb, "\u{00AD}", props), "w:sym" => { diff --git a/crates/engine/src/cmd/connectors.rs b/crates/engine/src/cmd/connectors.rs index 753ec88d8..1746c26ca 100644 --- a/crates/engine/src/cmd/connectors.rs +++ b/crates/engine/src/cmd/connectors.rs @@ -313,6 +313,22 @@ mod tests { } } + /// The per-case shape/geometry of one scaled-group reroute case. + struct ScaledGroupSpec { + w: f32, + h: f32, + kind: ShapeKind, + rot: f32, + flip_h: bool, + flip_v: bool, + } + + impl ScaledGroupSpec { + fn new(w: f32, h: f32, kind: ShapeKind, rot: f32, flip_h: bool, flip_v: bool) -> Self { + Self { w, h, kind, rot, flip_h, flip_v } + } + } + fn visual_ends(group: &InlineObject) -> ((f32, f32), (f32, f32)) { let InlineObject::Group { w, h, float, .. } = group else { panic!("group") }; let rects = group.group_rects(0.0, 0.0, *w, *h); @@ -327,6 +343,61 @@ mod tests { (turn((x, y)), turn((x + w, y + h))) } + /// One scaled-group case: rerouting keeps the visual free end and the resolved sites. + fn assert_scaled_group_reroute(spec: ScaledGroupSpec, free: Option, free_start: bool) { + let attached = Some(ConnEnd { id: 7, site: 3 }); + let (start, end) = if free_start { (free, attached) } else { (attached, free) }; + let group = InlineObject::Group { + w: spec.w, + h: spec.h, + ch_w: 200.0, + ch_h: 300.0, + float: Float { rot: 25.0, flip_h: true, ..Default::default() }, + canvas: None, + children: vec![ + GroupChild { x: 100.0, y: 200.0, obj: shape(ShapeKind::Rectangle, ShapeExtra { id: 7, ..Default::default() }, Float::default()) }, + GroupChild { + x: 10.0, + y: 30.0, + obj: shape( + spec.kind, + ShapeExtra { start, end, ..Default::default() }, + Float { rot: spec.rot, flip_h: spec.flip_h, flip_v: spec.flip_v, ..Default::default() }, + ), + }, + ], + }; + // Rendering scales each child rectangle before applying its own spin. + let before = visual_ends(&group); + let expected = if free_start { before.0 } else { before.1 }; + let mut s = Session::new(wordcraft_doc::Document::new()); + let pos = s.doc.start_of(StoryRef::Body); + s.doc.para_mut(pos.story, &pos.path).unwrap().insert_object(pos.off, group, &Default::default()).unwrap(); + assert!(reroute(&mut s)); + let group = s.doc.para_at(&pos).unwrap().object_at(pos.off).unwrap(); + let after = visual_ends(group); + let actual = if free_start { after.0 } else { after.1 }; + assert!( + (actual.0 - expected.0).abs() < 0.001 && (actual.1 - expected.1).abs() < 0.001, + "{}x{}, {:?}, {}, {}, {}, {}: {:?} != {:?}", + spec.w, + spec.h, + spec.kind, + spec.rot, + spec.flip_h, + spec.flip_v, + free_start, + actual, + expected + ); + let InlineObject::Group { children, .. } = group else { panic!("group") }; + let c = &children[1]; + let InlineObject::Shape { w, h, float, .. } = &c.obj else { panic!("connector") }; + let routed = ends(c.x, c.y, *w, *h, float.flip_h, float.flip_v); + assert_eq!(if free_start { routed.1 } else { routed.0 }, (140.0, 210.0), "resolved sites stay in child coordinates"); + assert_eq!(float.rot, 0.0); + assert!(!reroute(&mut s), "rerouting must settle"); + } #[test] fn rerouting_scaled_group_preserves_visual_free_and_unresolved_ends() { for (w, h) in [(200.0, 300.0), (400.0, 600.0), (400.0, 150.0)] { @@ -335,53 +406,7 @@ mod tests { for (flip_h, flip_v) in [(false, false), (true, false), (false, true), (true, true)] { for free in [None, Some(ConnEnd { id: 404, site: 0 }), Some(ConnEnd { id: 7, site: 99 })] { for free_start in [false, true] { - let attached = Some(ConnEnd { id: 7, site: 3 }); - let (start, end) = if free_start { (free, attached) } else { (attached, free) }; - let group = InlineObject::Group { - w, - h, - ch_w: 200.0, - ch_h: 300.0, - float: Float { rot: 25.0, flip_h: true, ..Default::default() }, - canvas: None, - children: vec![ - GroupChild { - x: 100.0, - y: 200.0, - obj: shape(ShapeKind::Rectangle, ShapeExtra { id: 7, ..Default::default() }, Float::default()), - }, - GroupChild { - x: 10.0, - y: 30.0, - obj: shape( - kind, - ShapeExtra { start, end, ..Default::default() }, - Float { rot, flip_h, flip_v, ..Default::default() }, - ), - }, - ], - }; - // Rendering scales each child rectangle before applying its own spin. - let before = visual_ends(&group); - let expected = if free_start { before.0 } else { before.1 }; - let mut s = Session::new(wordcraft_doc::Document::new()); - let pos = s.doc.start_of(StoryRef::Body); - s.doc.para_mut(pos.story, &pos.path).unwrap().insert_object(pos.off, group, &Default::default()).unwrap(); - assert!(reroute(&mut s)); - let group = s.doc.para_at(&pos).unwrap().object_at(pos.off).unwrap(); - let after = visual_ends(group); - let actual = if free_start { after.0 } else { after.1 }; - assert!( - (actual.0 - expected.0).abs() < 0.001 && (actual.1 - expected.1).abs() < 0.001, - "{w}x{h}, {kind:?}, {rot}, {flip_h}, {flip_v}, {free_start}: {actual:?} != {expected:?}" - ); - let InlineObject::Group { children, .. } = group else { panic!("group") }; - let c = &children[1]; - let InlineObject::Shape { w, h, float, .. } = &c.obj else { panic!("connector") }; - let routed = ends(c.x, c.y, *w, *h, float.flip_h, float.flip_v); - assert_eq!(if free_start { routed.1 } else { routed.0 }, (140.0, 210.0), "resolved sites stay in child coordinates"); - assert_eq!(float.rot, 0.0); - assert!(!reroute(&mut s), "rerouting must settle"); + assert_scaled_group_reroute(ScaledGroupSpec::new(w, h, kind, rot, flip_h, flip_v), free, free_start); } } } diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 15c6f9bb9..1cccd5e23 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -401,19 +401,9 @@ fn target_has_highlight(s: &Session, h: Highlight) -> bool { if let Some(rows) = column_rows(s) { for (a, b) in rows { let Some(p) = s.doc.para_at(&a) else { continue }; - if a == b || (a.off == 0 && b.off == p.len()) { - seen = true; - if !matches(p.props.style.as_deref(), &p.mark) { - return false; - } - } - for (r, c) in p.run_ranges() { - if r.end > a.off && r.start < b.off { - seen = true; - if !matches(p.props.style.as_deref(), c) { - return false; - } - } + let mark = a == b || (a.off == 0 && b.off == p.len()); + if !span_matches(p, mark, a.off, b.off, &matches, &mut seen) { + return false; } } if seen { @@ -426,19 +416,9 @@ fn target_has_highlight(s: &Session, h: Highlight) -> bool { let from = if path == a.path { a.off } else { 0 }; let to = if path == b.path { b.off } else { p.len() }; // Match format_range: fully selected paragraphs include their mark. - if to == p.len() && (from == 0 || path != a.path) { - seen = true; - if !matches(p.props.style.as_deref(), &p.mark) { - return false; - } - } - for (r, c) in p.run_ranges() { - if r.end > from && r.start < to { - seen = true; - if !matches(p.props.style.as_deref(), c) { - return false; - } - } + let mark = to == p.len() && (from == 0 || path != a.path); + if !span_matches(p, mark, from, to, &matches, &mut seen) { + return false; } } } @@ -446,6 +426,34 @@ fn target_has_highlight(s: &Session, h: Highlight) -> bool { seen || resolved(s).iter().all(|r| r.highlight == h.rgb()) } +/// Whether the paragraph's mark (when `mark`) and the runs overlapping `from..to` all match +/// `matches`; `seen` records that something was checked. +fn span_matches( + p: &wordcraft_doc::Paragraph, + mark: bool, + from: usize, + to: usize, + matches: &impl Fn(Option<&str>, &CharProps) -> bool, + seen: &mut bool, +) -> bool { + let style = p.props.style.as_deref(); + if mark { + *seen = true; + if !matches(style, &p.mark) { + return false; + } + } + for (r, c) in p.run_ranges() { + if r.end > from && r.start < to { + *seen = true; + if !matches(style, c) { + return false; + } + } + } + true +} + /// Explicit palette/automation colours set highlighting; the shortcut (no colour) toggles /// the last choice off only when the whole formatting target already has that colour. fn highlight(s: &mut Session, v: &Value) -> CmdResult { diff --git a/crates/engine/src/cmd/outline.rs b/crates/engine/src/cmd/outline.rs index 0cf5727cb..571776b16 100644 --- a/crates/engine/src/cmd/outline.rs +++ b/crates/engine/src/cmd/outline.rs @@ -460,6 +460,51 @@ pub(crate) fn move_shown(s: &mut Session, up: bool) -> Option { Some(sel_result(s)) } +/// Keeps collapsed headings collapsed after an edit: each one moves to the nearest heading with the +/// same text that is still free. +fn remap_collapsed_headings(s: &mut Session) { + let hashes: Vec> = (0..s.doc.body.len()).map(|i| text_hash(&s.doc, i)).collect(); + let levels = ol::levels(&s.doc); + // The headings not yet taken, by text hash. + let mut by_hash: HashMap> = HashMap::new(); + for (j, h) in hashes.iter().enumerate() { + if let Some(h) = h + && levels.get(j).is_some_and(|l| *l < BODY) + { + by_hash.entry(*h).or_default().insert(j); + } + } + let mut used = BTreeSet::new(); + let mut out = Vec::new(); + for (i, h) in std::mem::take(&mut s.view.outline.collapsed) { + let free = |j: usize, used: &BTreeSet| levels.get(j).is_some_and(|l| *l < BODY) && !used.contains(&j); + let same = |j: usize| hashes.get(j).copied().flatten() == Some(h); + let found = if free(i, &used) && same(i) { + Some(i) + } else { + // The nearest free heading with the same text (the earlier one on a tie). + let nearest = by_hash.get(&h).and_then(|set| { + let below = set.range(..=i).next_back().copied(); + let above = set.range(i..).next().copied(); + match (below, above) { + (Some(b), Some(a)) => Some(if a.abs_diff(i) < b.abs_diff(i) { a } else { b }), + (b, a) => b.or(a), + } + }); + nearest.or_else(|| free(i, &used).then_some(i)) + }; + if let Some(j) = found { + used.insert(j); + if let Some(set) = hashes.get(j).copied().flatten().and_then(|hj| by_hash.get_mut(&hj)) { + set.remove(&j); + } + out.push((j, hashes.get(j).copied().flatten().unwrap_or(h))); + } + } + out.sort_unstable(); + s.view.outline.collapsed = out; +} + /// After every command: collapsed headings are found again where edits moved them (by their /// text; one whose text changed in place stays collapsed), and in Outline view a caret the /// outline doesn't show (in collapsed text, or past a body paragraph's first line) moves on to @@ -467,46 +512,7 @@ pub(crate) fn move_shown(s: &mut Session, up: bool) -> Option { /// `changed` says whether the document changed (its revision moved); if not, nothing moved. pub(crate) fn after_command(s: &mut Session, before: &Selection, changed: bool) { if changed && !s.view.outline.collapsed.is_empty() { - let hashes: Vec> = (0..s.doc.body.len()).map(|i| text_hash(&s.doc, i)).collect(); - let levels = ol::levels(&s.doc); - // The headings not yet taken, by text hash. - let mut by_hash: HashMap> = HashMap::new(); - for (j, h) in hashes.iter().enumerate() { - if let Some(h) = h - && levels.get(j).is_some_and(|l| *l < BODY) - { - by_hash.entry(*h).or_default().insert(j); - } - } - let mut used = BTreeSet::new(); - let mut out = Vec::new(); - for (i, h) in std::mem::take(&mut s.view.outline.collapsed) { - let free = |j: usize, used: &BTreeSet| levels.get(j).is_some_and(|l| *l < BODY) && !used.contains(&j); - let same = |j: usize| hashes.get(j).copied().flatten() == Some(h); - let found = if free(i, &used) && same(i) { - Some(i) - } else { - // The nearest free heading with the same text (the earlier one on a tie). - let nearest = by_hash.get(&h).and_then(|set| { - let below = set.range(..=i).next_back().copied(); - let above = set.range(i..).next().copied(); - match (below, above) { - (Some(b), Some(a)) => Some(if a.abs_diff(i) < b.abs_diff(i) { a } else { b }), - (b, a) => b.or(a), - } - }); - nearest.or_else(|| free(i, &used).then_some(i)) - }; - if let Some(j) = found { - used.insert(j); - if let Some(set) = hashes.get(j).copied().flatten().and_then(|hj| by_hash.get_mut(&hj)) { - set.remove(&j); - } - out.push((j, hashes.get(j).copied().flatten().unwrap_or(h))); - } - } - out.sort_unstable(); - s.view.outline.collapsed = out; + remap_collapsed_headings(s); } if s.view.mode != ViewMode::Outline || !s.sel.is_collapsed() || s.sel.focus.story != StoryRef::Body { return; diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 9fcaa98a4..5af39f7a2 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -606,6 +606,44 @@ fn resolve_sel_then(s: &mut Session, v: &Value, accept: bool) -> CmdResult { resolve_sel(s, accept, p::bool(v, "next") == Some(true)) } +/// The span of the tracked change around `off` in `p`: its run, joined across the comment anchors +/// that split one revision's text into several runs. +fn revision_span(p: &Paragraph, off: usize) -> Option> { + let runs: Vec<_> = p.run_ranges().collect(); + let i = runs.iter().position(|(r, c)| r.start <= off && off <= r.end && (c.ins.is_some() || c.del.is_some() || c.fmt_change.is_some()))?; + let (mut start, mut end) = (runs[i].0.start, runs[i].0.end); + let key = (runs[i].1.ins, runs[i].1.del); + if key == (None, None) { + return Some(start..end); + } + // Comment anchors split a revision's text into several runs; the revision goes on past them. + let is_anchor = |k: usize| matches!(p.object_at(runs[k].0.start), Some(InlineObject::CommentStart { .. } | InlineObject::CommentEnd { .. })); + let same = |k: usize| (runs[k].1.ins, runs[k].1.del) == key; + let mut k = i; + while k > 0 { + if same(k - 1) { + k -= 1; + } else if is_anchor(k - 1) && k > 1 && same(k - 2) { + k -= 2; + } else { + break; + } + start = runs[k].0.start; + } + let mut k = i; + while k + 1 < runs.len() { + if same(k + 1) { + k += 1; + } else if is_anchor(k + 1) && k + 2 < runs.len() && same(k + 2) { + k += 2; + } else { + break; + } + end = runs[k].0.end; + } + Some(start..end) +} + fn resolve_sel(s: &mut Session, accept: bool, next: bool) -> CmdResult { let (a, b) = s.sel.ordered(); // A caret right before a tracked paragraph mark (and no tracked text around it): that mark. @@ -616,44 +654,7 @@ fn resolve_sel(s: &mut Session, accept: bool, next: bool) -> CmdResult { let (a, b) = if a == b { // The change at the caret: the run around it, else the paragraph mark right after it, // else the paragraph's formatting, else the table cell's. - let found = s.doc.para_at(&a).and_then(|p| { - let runs: Vec<_> = p.run_ranges().collect(); - let i = runs - .iter() - .position(|(r, c)| r.start <= a.off && a.off <= r.end && (c.ins.is_some() || c.del.is_some() || c.fmt_change.is_some()))?; - let (mut start, mut end) = (runs[i].0.start, runs[i].0.end); - let key = (runs[i].1.ins, runs[i].1.del); - if key == (None, None) { - return Some(start..end); - } - // Comment anchors split a revision's text into several runs; the revision goes on past them. - let is_anchor = - |k: usize| matches!(p.object_at(runs[k].0.start), Some(InlineObject::CommentStart { .. } | InlineObject::CommentEnd { .. })); - let same = |k: usize| (runs[k].1.ins, runs[k].1.del) == key; - let mut k = i; - while k > 0 { - if same(k - 1) { - k -= 1; - } else if is_anchor(k - 1) && k > 1 && same(k - 2) { - k -= 2; - } else { - break; - } - start = runs[k].0.start; - } - let mut k = i; - while k + 1 < runs.len() { - if same(k + 1) { - k += 1; - } else if is_anchor(k + 1) && k + 2 < runs.len() && same(k + 2) { - k += 2; - } else { - break; - } - end = runs[k].0.end; - } - Some(start..end) - }); + let found = s.doc.para_at(&a).and_then(|p| revision_span(p, a.off)); let para_fmt = s.doc.para_at(&a).is_some_and(|p| p.props.fmt_change.is_some() || p.props.num_change.is_some()); let mark_fmt = s.doc.para_at(&a).is_some_and(|p| a.off == p.len() && p.mark.fmt_change.is_some()); match found { diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 9a1762866..f0e10e843 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -899,7 +899,7 @@ fn tab_indent_on(s: &Session) -> bool { fn tab_indents(s: &mut Session, f: &Pos) -> bool { let Some(para) = s.doc.para_at(f).filter(|p| !p.is_empty()) else { return false }; if f.off == 0 { - return !s.doc.styles.resolve_para(¶.props).numbering.is_some_and(|n| n.num != 0); + return s.doc.styles.resolve_para(¶.props).numbering.is_none_or(|n| n.num == 0); } let l = s.layout(); l.line_bounds(f, s.page_hint).is_some_and(|(a, _)| a == *f) diff --git a/crates/fonts/src/tests_sysfonts.rs b/crates/fonts/src/tests_sysfonts.rs index 5a0912c4b..132535456 100644 --- a/crates/fonts/src/tests_sysfonts.rs +++ b/crates/fonts/src/tests_sysfonts.rs @@ -1,5 +1,5 @@ //! Installed system fonts: found by family name by every lookup, whatever ran before it (#22). - +#[allow(unused_imports)] use std::path::{Path, PathBuf}; use super::*; diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 6771be76b..dc27ea8ea 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -4528,7 +4528,7 @@ fn place_table(ctx: &mut Ctx, pb: &mut PageBuilder, t: &wordcraft_doc::Table, bl && !pb.at_top() && !fresh && rest.is_none() - && !ri.checked_sub(1).and_then(|prev| kept.get(prev)).is_some_and(|&(end, _)| end >= ri) + && ri.checked_sub(1).and_then(|prev| kept.get(prev)).is_none_or(|&(end, _)| end < ri) && kept.get(ri).is_some_and(|&(end, height)| { if end == ri { return false; diff --git a/crates/layout/src/tests_bidi.rs b/crates/layout/src/tests_bidi.rs index 8aae6c129..548ca6c9b 100644 --- a/crates/layout/src/tests_bidi.rs +++ b/crates/layout/src/tests_bidi.rs @@ -1,6 +1,7 @@ //! Right-to-left and bidirectional layout: Persian paragraphs, mixed Persian/Latin lines, //! numbers and punctuation, caret and selection geometry. +#[allow(unused_imports)] use super::*; use crate::para::{bidi_levels, visual_order}; use wordcraft_doc::props::{Align, ParaProps}; diff --git a/crates/layout/src/tests_grid.rs b/crates/layout/src/tests_grid.rs index fc2c9005a..de0850ce2 100644 --- a/crates/layout/src/tests_grid.rs +++ b/crates/layout/src/tests_grid.rs @@ -1,6 +1,7 @@ //! Document grid (`w:docGrid`): line pitch and character pitch (#391). Assertions are relations //! to the pitch, never font metrics (fonts differ between machines; CJK glyphs may be missing). +#[allow(unused_imports)] use super::*; use wordcraft_doc::props::LineSpacing; use wordcraft_doc::section::{DocGrid, DocGridType}; diff --git a/crates/layout/src/tests_textbox.rs b/crates/layout/src/tests_textbox.rs index 5148e770c..96884ac0c 100644 --- a/crates/layout/src/tests_textbox.rs +++ b/crates/layout/src/tests_textbox.rs @@ -1,5 +1,6 @@ //! Text boxes' text direction, alignment and linked boxes (Shape Format › Text). +#[allow(unused_imports)] use super::*; use wordcraft_doc::props::{TextVert, VAlign}; use wordcraft_doc::{PartKind, Pos, TextBody, para_block}; diff --git a/crates/proof/src/affix.rs b/crates/proof/src/affix.rs index 03fc27a6b..4ec068466 100644 --- a/crates/proof/src/affix.rs +++ b/crates/proof/src/affix.rs @@ -314,13 +314,12 @@ fn parse_aff(aff: &str) -> Result { match dir { "SET" | "LANG" | "NAME" | "VERSION" | "HOME" | "WORDCHARS" | "MAXNGRAMSUGS" | "MAXDIFF" | "ONLYMAXDIFF" | "NOSPLITSUGS" | "SUGSWITHDOTS" | "WARN" | "FORBIDWARN" | "MAXCPDSUGS" => {} - "FLAG" => { - // Per-character flag modes (`FLAG UTF-8`, as VERO's pt_BR dictionary uses) work - // with the one-character parsing here; only the multi-character encodings differ. - if !matches!(at(1), "UTF-8" | "DEFAULT" | "utf-8" | "default" | "UTF8" | "") { - return Err(format!("line {}: FLAG {} is not supported (one-character flags only)", n + 1, at(1))); - } + // Per-character flag modes (`FLAG UTF-8`, as VERO's pt_BR dictionary uses) work + // with the one-character parsing here; only the multi-character encodings differ. + "FLAG" if !matches!(at(1), "UTF-8" | "DEFAULT" | "utf-8" | "default" | "UTF8" | "") => { + return Err(format!("line {}: FLAG {} is not supported (one-character flags only)", n + 1, at(1))); } + "FLAG" => {} directive @ ("FORBIDDENWORD" | "NOSUGGEST") => { let mut it = at(1).chars().filter(|&c| c != EMOJI_SELECTOR); match (it.next(), it.next()) { diff --git a/crates/ui-egui/src/dialogs_layout.rs b/crates/ui-egui/src/dialogs_layout.rs index e0e0f5222..f983ed8cb 100644 --- a/crates/ui-egui/src/dialogs_layout.rs +++ b/crates/ui-egui/src/dialogs_layout.rs @@ -952,7 +952,7 @@ fn alert(ui: &mut Ui, text: &Option) { fn split_cells_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SplitCellsForm) -> bool { // Several cells split one by one keep their rows. - let rows_on = !(f.cells > 1 && !f.merge_first); + let rows_on = f.cells <= 1 || f.merge_first; egui::Grid::new("split_cells").num_columns(2).show(ui, |ui| { ui.label(tl!("Number of columns:")); ui.add(egui::DragValue::new(&mut f.columns).range(1..=63)); diff --git a/crates/ui-egui/src/keytips.rs b/crates/ui-egui/src/keytips.rs index a874e2156..7ecc99039 100644 --- a/crates/ui-egui/src/keytips.rs +++ b/crates/ui-egui/src/keytips.rs @@ -773,7 +773,7 @@ pub fn logic(app: &mut WordApp, ctx: &egui::Context) { } // Alt off while an Alt key is still down (AltGr on Linux turns Level 3 Shift on, not // Alt): the key's release ends the press, not this. - egui::Event::ModifiersChanged(modifiers) if !(app.ui.alt_key_down && !modifiers.alt) => { + egui::Event::ModifiersChanged(modifiers) if !app.ui.alt_key_down || modifiers.alt => { alt_released |= alt_transition(app, modifiers.alt, *modifiers); } egui::Event::PointerButton { pressed: true, .. } => { diff --git a/crates/ui-egui/src/objects.rs b/crates/ui-egui/src/objects.rs index c60737859..afc83fcd2 100644 --- a/crates/ui-egui/src/objects.rs +++ b/crates/ui-egui/src/objects.rs @@ -272,7 +272,7 @@ pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, pages: &[Rect] let adding = ui.input(|i| i.modifiers.shift || i.modifiers.command); // Ctrl on the object that is already selected starts a copy-drag instead. let again = selected(app).is_some_and(|(p, _)| p == object.pos()); - if pressed && adding && grab == Grab::Move && selected(app).is_some() && !(again && !ui.input(|i| i.modifiers.shift)) { + if pressed && adding && grab == Grab::Move && selected(app).is_some() && (ui.input(|i| i.modifiers.shift) || !again) { let _ = app.run("select.addObject", json!({"pos": object.pos()})); return true; } diff --git a/xtask/src/visual_diff.rs b/xtask/src/visual_diff.rs index 20f857c98..b0259af88 100644 --- a/xtask/src/visual_diff.rs +++ b/xtask/src/visual_diff.rs @@ -37,7 +37,7 @@ pub fn run(args: &[&str]) -> Result<(), String> { None => 100, }; let pos: Vec<&str> = - args.iter().enumerate().filter(|(i, a)| !a.starts_with("--") && !(*i > 0 && args[i - 1] == "--dpi")).map(|(_, a)| *a).collect(); + args.iter().enumerate().filter(|(i, a)| !(a.starts_with("--") || *i > 0 && args[i - 1] == "--dpi")).map(|(_, a)| *a).collect(); let [reference, test, out] = pos.as_slice() else { return Err("usage: cargo xtask visual-diff REF.pdf TEST.pdf OUTDIR [--private] [--dpi N]".into()); };