Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor cb49ac055d Sonar: fix clippy lints that fail the gate; drop unread ScaledGroupSpec field
Collapse nested ifs into match guards, simplify negated boolean
expressions to their equivalent forms, and remove the ScaledGroupSpec
group field, which was written but never read.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-26 09:06:48 -04:00
Dylan M. Taylor f2c32842bc Sonar: reduce assert_scaled_group_reroute to 3 params
Wrap the 6 shape/geometry arguments (w, h, kind, rot, flip_h,
flip_v) of the reroute test helper into a ScaledGroupSpec config
struct so the function drops under Sonar's rust:S107 7-parameter
threshold (it was 8). The call site builds the spec inline.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-26 08:46:06 -04:00
Dylan M. Taylor 7b0e079705 Sonar: extract helpers from highlight check, revision span and outline remap
Behaviour-preserving extractions for S3776:
- engine cmd/format.rs target_has_highlight: span_matches checks a paragraph's mark and runs.
- engine cmd/review.rs resolve_sel: revision_span finds the tracked change at a caret.
- engine cmd/outline.rs after_command: remap_collapsed_headings keeps collapsed headings.
2026-10-26 08:46:03 -04:00
18 changed files with 216 additions and 181 deletions
+1 -1
View File
@@ -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`).
+6 -12
View File
@@ -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),
+3 -4
View File
@@ -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" => {
+72 -47
View File
@@ -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<ConnEnd>, 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);
}
}
}
+34 -26
View File
@@ -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 {
+46 -40
View File
@@ -460,6 +460,51 @@ pub(crate) fn move_shown(s: &mut Session, up: bool) -> Option<CmdResult> {
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<Option<u64>> = (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<u64, BTreeSet<usize>> = 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<usize>| 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<CmdResult> {
/// `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<Option<u64>> = (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<u64, BTreeSet<usize>> = 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<usize>| 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;
+39 -38
View File
@@ -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<std::ops::Range<usize>> {
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 {
+1 -1
View File
@@ -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(&para.props).numbering.is_some_and(|n| n.num != 0);
return s.doc.styles.resolve_para(&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)
+1 -1
View File
@@ -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::*;
+1 -1
View File
@@ -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;
+1
View File
@@ -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};
+1
View File
@@ -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};
+1
View File
@@ -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};
+5 -6
View File
@@ -314,13 +314,12 @@ fn parse_aff(aff: &str) -> Result<Rules, String> {
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()) {
+1 -1
View File
@@ -952,7 +952,7 @@ fn alert(ui: &mut Ui, text: &Option<String>) {
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));
+1 -1
View File
@@ -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, .. } => {
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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());
};