Drop Cap Options dialog (#2601) (#2602)

* #2601: Drop Cap Options dialog

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* gaps: keep the Drop Cap Options note on the dialog-depth row

* i18n: translate the two Drop Cap disabled reasons in the audited catalogs

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-18 11:44:30 -04:00
1 parent 87cab84e4a
commit beedd1b30d
42 files changed
+670 -23

No files matched your search

+2
View File
@@ -178,6 +178,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
| 2026-10-16 | Footnote and endnote editing keeps the body and active note text at normal opacity (#2545), without changing document colours or print/PDF output. |
| 2026-10-16 | A picture watermark in a header that starts with a table is drawn centred on the page behind the body text, on every page, instead of in the header's cell at the top of the page (#1775); `layoutInCell="0"` and VML `o:allowincell="f"` survive save and reopen |
| 2026-10-17 | Ribbon hover hints: a disabled control says why (the command's own reason), split-button arrows, menu buttons and the font, size and outline boxes have hints, and hints show on keyboard focus (#1086). |
| 2026-10-17 | Insert › Drop Cap is a menu (None, Dropped, In margin, Drop Cap Options…); the dialog sets position, font, lines to drop and distance; letters in the margin; DOCX keeps them (#2601). |
|---|---|
| 2026-10-16 | Save Changes gives assistive technology a named alert dialog with the loss warning and any save failure; visual layout and save policy unchanged (#1015). Native screen-reader verification remains pending. |
| 2026-10-15 | The font family caches are bounded (4096 families for text, 1024 for math, names over 256 bytes aren't remembered), so a document naming thousands of missing fonts can't grow memory; equation fonts are looked up again after a font is added or removed instead of keeping the old choice (#1354) |
@@ -541,6 +542,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
| 2026-10-15 | trivial | Progress log: the word being typed gets no squiggle until the caret leaves it (#1552) |
| 2026-10-15 | trivial | Progress log: Highlight No Color sticks to the button (#2105) |
| 2026-10-16 | trivial | Body and active note text keep normal opacity while editing footnotes or endnotes (#2545); header/footer dimming is unchanged |
| 2026-10-17 | minor | Progress log: Drop Cap Options (#2601) |
| 2026-10-14 | trivial | Progress log: row table border exceptions |
| 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks |
| 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged |
+11 -1
View File
@@ -919,6 +919,16 @@ pub struct ParaProps {
/// Drop cap: lines to drop (0 = none) — applies to the first character(s).
#[serde(skip_serializing_if = "Option::is_none")]
pub drop_cap: Option<u8>,
/// Drop cap "In margin" (`w:dropCap="margin"`): the letter hangs in the left margin and the
/// text keeps its indent. Unset = dropped into the text.
#[serde(skip_serializing_if = "Option::is_none")]
pub drop_cap_margin: Option<bool>,
/// The drop cap's distance from the text, in points (`w:hSpace`). Unset = 0.
#[serde(skip_serializing_if = "Option::is_none")]
pub drop_cap_distance: Option<f32>,
/// The drop cap letter's font family (the cap run's `w:rFonts`). Unset = the text's font.
#[serde(skip_serializing_if = "Option::is_none")]
pub drop_cap_font: Option<String>,
/// A text frame (`w:framePr` other than a drop cap): the paragraph is placed apart from the
/// text, which may wrap around it — a footer's page number in a frame, for instance.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -959,7 +969,7 @@ impl ParaProps {
pub fn overlay(&mut self, patch: &ParaProps) {
overlay_fields!(self, patch; style, align, indent_left, indent_right, indent_first, indent_left_chars, indent_right_chars, indent_first_chars, space_before, space_after, space_before_auto, space_after_auto, line_spacing,
contextual_spacing, mirror_indents, keep_next, keep_lines, page_break_before, widow_control, outline_level, numbering, tabs, shading, borders,
suppress_hyphens, suppress_line_numbers, bidi, drop_cap, frame, kinsoku, word_wrap, overflow_punct, top_line_punct, auto_space_de, auto_space_dn,
suppress_hyphens, suppress_line_numbers, bidi, drop_cap, drop_cap_margin, drop_cap_distance, drop_cap_font, frame, kinsoku, word_wrap, overflow_punct, top_line_punct, auto_space_de, auto_space_dn,
snap_to_grid);
}
pub fn overlaid(mut self, patch: &ParaProps) -> ParaProps {
+14
View File
@@ -11,6 +11,8 @@ use crate::styles::{StyleKind, StyleSheet};
/// Automatic (HTML-style) paragraph spacing, points: what Word for Windows gives `w:beforeAutospacing`
/// and `w:afterAutospacing` at any font size (measured).
pub const AUTO_SPACING: f32 = 14.0;
/// The largest drop cap distance from the text, in points (22", as Word's dialog allows).
pub const MAX_DROP_CAP_DISTANCE: f32 = 1584.0;
/// Concrete character formatting.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -178,6 +180,15 @@ pub struct ResolvedPara {
pub borders: Option<Borders>,
pub bidi: bool,
pub drop_cap: u8,
/// The drop cap hangs in the margin (see `ParaProps::drop_cap_margin`).
#[serde(default)]
pub drop_cap_margin: bool,
/// The drop cap's distance from the text, in points.
#[serde(default)]
pub drop_cap_distance: f32,
/// The drop cap letter's font family; `None` = the text's.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub drop_cap_font: Option<String>,
/// A text frame the paragraph sits in (see `ParaProps::frame`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frame: Option<crate::props::TextFrame>,
@@ -256,6 +267,9 @@ impl ResolvedPara {
borders: p.borders.filter(Borders::any_visible),
bidi: p.bidi.unwrap_or(false),
drop_cap: p.drop_cap.unwrap_or(0).min(10),
drop_cap_margin: p.drop_cap_margin.unwrap_or(false),
drop_cap_distance: p.drop_cap_distance.filter(|d| d.is_finite()).unwrap_or(0.0).clamp(0.0, MAX_DROP_CAP_DISTANCE),
drop_cap_font: p.drop_cap_font.clone().filter(|f| !f.trim().is_empty()),
frame: p.frame.clone().filter(|_| p.drop_cap.unwrap_or(0) == 0),
suppress_hyphens: p.suppress_hyphens.unwrap_or(false),
suppress_line_numbers: p.suppress_line_numbers.unwrap_or(false),
+4 -1
View File
@@ -269,8 +269,11 @@ impl PropCtx {
"w:autoSpaceDN" => p.auto_space_dn = Some(on_off(k)),
"w:snapToGrid" => p.snap_to_grid = Some(on_off(k)),
"w:framePr" => {
if matches!(k.attr("w:dropCap"), Some("drop") | Some("margin")) {
if let Some(kind @ ("drop" | "margin")) = k.attr("w:dropCap") {
p.drop_cap = Some(k.attr("w:lines").and_then(u32_of).unwrap_or(3).clamp(1, 10) as u8);
p.drop_cap_margin = (kind == "margin").then_some(true);
p.drop_cap_distance =
tw(k, "w:hSpace").filter(|d| d.is_finite() && *d > 0.0).map(|d| d.min(wordcraft_doc::resolve::MAX_DROP_CAP_DISTANCE));
} else {
p.frame = Some(text_frame(k));
}
+13 -2
View File
@@ -201,10 +201,21 @@ impl Reader<'_> {
&& p.props.drop_cap.is_none()
&& let Some(Block::Para(mut first)) = out.pop().map(|b| (*b).clone())
{
let lines = first.props.drop_cap;
let mut props = p.props.clone();
props.drop_cap = first.props.drop_cap;
props.drop_cap_margin = first.props.drop_cap_margin;
props.drop_cap_distance = first.props.drop_cap_distance;
// A letter in a font of its own: the drop cap's font, the letter itself
// in the text's (so taking the drop cap away gives the letter its font back).
let cap_font = first.runs.first().and_then(|r| r.props.font.clone());
let text_font = p.runs.first().and_then(|r| r.props.font.clone());
if cap_font.is_some() && cap_font != text_font {
for r in &mut first.runs {
r.props.font = text_font.clone();
}
props.drop_cap_font = cap_font;
}
first.append(p);
props.drop_cap = lines;
first.props = props;
out.push(para_block(first));
continue;
+11 -4
View File
@@ -247,10 +247,17 @@ pub fn ppr_inner(w: &mut W, p: &ParaProps, framed: bool, num_change: Option<&Cha
toggle(w, "w:keepLines", p.keep_lines);
toggle(w, "w:pageBreakBefore", p.page_break_before);
if framed && let Some(lines) = p.drop_cap.filter(|l| *l > 0) {
w.empty(
"w:framePr",
&[("w:dropCap", "drop"), ("w:lines", &n(lines.min(10) as i64)), ("w:wrap", "around"), ("w:vAnchor", "text"), ("w:hAnchor", "text")],
);
// ECMA-376 §17.3.1.11: the drop cap's frame — dropped or in the margin, the lines it
// spans and its distance from the text (`w:hSpace`, twips).
let kind = if p.drop_cap_margin == Some(true) { "margin" } else { "drop" };
let lines = n(lines.min(10) as i64);
let space = p.drop_cap_distance.filter(|d| d.is_finite() && *d > 0.0).map(|d| twips(d.min(wordcraft_doc::resolve::MAX_DROP_CAP_DISTANCE)));
let mut a: Vec<(&str, &str)> = vec![("w:dropCap", kind), ("w:lines", &lines)];
if let Some(h) = &space {
a.push(("w:hSpace", h));
}
a.extend([("w:wrap", "around"), ("w:vAnchor", "text"), ("w:hAnchor", "text")]);
w.empty("w:framePr", &a);
} else if let Some(f) = &p.frame {
let (fw, fh, fx, fy) = (f.w.map(twips), f.h.map(twips), twips(f.x), twips(f.y));
let (hs, vs) = (twips(f.h_space), twips(f.v_space));
+6
View File
@@ -139,6 +139,12 @@ impl Writer<'_> {
let tail = head.split_off(first.len_utf8()).ok();
if let Some(mut tail) = tail {
head.props.drop_cap = Some(lines);
// The letter's own font goes on its run (`w:rFonts`).
if let Some(f) = p.props.drop_cap_font.as_ref().filter(|f| !f.trim().is_empty()) {
for r in &mut head.runs {
r.props.font = Some(f.clone());
}
}
head.section = None;
self.para_inner(w, &head, rels, false, depth, true);
tail.props.drop_cap = None;
+42
View File
@@ -175,6 +175,9 @@ fn every_para_prop_round_trips() {
suppress_line_numbers: Some(true),
bidi: Some(false),
drop_cap: None,
drop_cap_margin: None,
drop_cap_distance: None,
drop_cap_font: None,
frame: Some(wordcraft_doc::props::TextFrame {
w: Some(144.0),
h: Some(36.0),
@@ -2642,3 +2645,42 @@ fn page_background_picture_round_trips() {
// Saved again, it is still there.
assert!(rt(&r).settings.page_picture.is_some());
}
/// #2601: Drop Cap Options — a dropped cap with its lines, distance from the text and font, and
/// one in the margin, survive a save: `w:framePr w:dropCap="drop|margin" w:lines w:hSpace` on the
/// letter's own paragraph, the font on the letter's run (ECMA-376 §17.3.1.11). The text after the
/// letter keeps its own font.
#[test]
fn drop_cap_options_round_trip() {
let body = CharProps { font: Some("Liberation Serif".into()), ..Default::default() };
let mut dropped = Paragraph::with_text("Once upon a time", body.clone());
dropped.props.drop_cap = Some(2);
dropped.props.drop_cap_distance = Some(6.0);
dropped.props.drop_cap_font = Some("Liberation Mono".into());
let mut margin = Paragraph::with_text("In the margin", body.clone());
margin.props.drop_cap = Some(4);
margin.props.drop_cap_margin = Some(true);
let d = doc_with(vec![dropped, margin]);
let bytes = wordcraft_docx::write(&d).expect("write");
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
let mut xml = String::new();
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
assert!(xml.contains(r#"w:dropCap="drop" w:lines="2" w:hSpace="120""#), "{xml}");
assert!(xml.contains(r#"w:dropCap="margin" w:lines="4""#), "{xml}");
assert!(xml.contains(r#"w:ascii="Liberation Mono""#), "the letter's font on its run: {xml}");
let r = wordcraft_docx::read(&bytes).expect("read");
let got = paras(&r);
assert_eq!(got.len(), 2, "each letter merges back into its paragraph");
let (a, b) = (&got[0], &got[1]);
assert_eq!(a.plain_text(), "Once upon a time");
assert_eq!(
(a.props.drop_cap, a.props.drop_cap_margin, a.props.drop_cap_distance, a.props.drop_cap_font.as_deref()),
(Some(2), None, Some(6.0), Some("Liberation Mono"))
);
assert!(a.runs.iter().all(|r| r.props.font.as_deref() == Some("Liberation Serif")), "the letter keeps the text's font: {:?}", a.runs);
assert_eq!(b.plain_text(), "In the margin");
assert_eq!(
(b.props.drop_cap, b.props.drop_cap_margin, b.props.drop_cap_distance, b.props.drop_cap_font.as_deref()),
(Some(4), Some(true), None, None)
);
}
+70 -5
View File
@@ -106,11 +106,9 @@ pub fn specs() -> Vec<CommandSpec> {
.key(super::mac_or("Ctrl+Shift+T", "Alt+Shift+T")),
CommandSpec::new("insert.pageField", "Insert Page Field", "Insert › Text › Quick Parts", |s, _| field(s, &json!({"instr": "PAGE"})))
.key(super::mac_or("Ctrl+Shift+P", "Alt+Shift+P")),
CommandSpec::new("insert.dropCap", "Drop Cap", "Insert › Text", |s, v| {
let lines = p::u64(v, "lines").unwrap_or(3).min(10) as u8;
super::para::fmt(s, &|p| p.drop_cap = Some(lines))
})
.params(r#"{"lines"?: n (0 = none)}"#),
CommandSpec::new("insert.dropCap", "Drop Cap", "Insert › Text", drop_cap)
.params(r#"{"position"?: "none"|"dropped"|"margin" (default dropped), "lines"?: 1..10 (default 3; 0 = none), "distance"?: pt from the text (0..1584, default 0), "font"?: family (omit for the text's font)}"#)
.when(drop_cap_off),
CommandSpec::new("insert.horizontalLine", "Horizontal Line", "Home › Paragraph › Borders", |s, _| {
super::para::fmt(s, &|p| {
p.borders = Some(wordcraft_doc::props::Borders { bottom: Some(wordcraft_doc::props::Border::single(1.5)), ..Default::default() })
@@ -1224,3 +1222,70 @@ mod tests {
assert_eq!(date_picture("DATE"), None);
}
}
/// Why Insert › Drop Cap is off at the caret, as in Word: in a table cell, in an empty paragraph
/// and in one that starts with a space.
fn drop_cap_off(s: &Session) -> Option<&'static str> {
if s.sel.focus.path.cell().is_some() {
return Some("drop caps can't go in a table");
}
let first = s.doc.para_at(&s.sel.focus).and_then(|p| p.text.chars().next());
match first {
None => Some("the paragraph is empty"),
Some(c) if c.is_whitespace() => Some("the paragraph starts with a space"),
Some(_) => None,
}
}
/// Insert › Drop Cap and Drop Cap Options: none, or the letter dropped into the text or hung in
/// the margin, `lines` tall, `distance` points from the text, in `font` (unset: the text's).
/// None takes all of it away, the letter's font with it.
fn drop_cap(s: &mut Session, v: &Value) -> CmdResult {
let lines = v.get("lines").and_then(Value::as_f64).filter(|l| l.is_finite());
let none = p::str(v, "position") == Some("none") || lines == Some(0.0);
let margin = p::str(v, "position") == Some("margin");
let lines = lines.map_or(3.0, |l| l.round().clamp(1.0, 10.0)) as u8;
let distance = p::f32(v, "distance").map_or(0.0, |d| d.clamp(0.0, wordcraft_doc::resolve::MAX_DROP_CAP_DISTANCE));
// A family name, not a paragraph of text.
let font = p::str(v, "font").map(str::trim).filter(|f| !f.is_empty()).map(|f| f.chars().take(64).collect::<String>());
super::para::fmt(s, &|p| {
p.drop_cap = (!none).then_some(lines);
p.drop_cap_margin = (!none && margin).then_some(true);
p.drop_cap_distance = (!none && distance > 0.0).then_some(distance);
p.drop_cap_font = font.clone().filter(|_| !none);
})
}
/// Insert › Drop Cap at the caret: the menu's highlight and the Drop Cap dialog's values.
#[derive(Clone, Debug, PartialEq)]
pub struct DropCapState {
/// `none`, `dropped` or `margin`.
pub position: &'static str,
/// Lines to drop (3 when there is no drop cap).
pub lines: u8,
/// Distance from the text, points.
pub distance: f32,
/// The letter's own font, if it has one.
pub font: Option<String>,
/// The font of the paragraph's first character.
pub text_font: String,
}
pub fn drop_cap_state(s: &Session) -> DropCapState {
let para = s.doc.para_at(&s.sel.focus);
let props = para.map(|p| p.props.clone()).unwrap_or_default();
let rp = s.doc.styles.resolve_para(&props);
let first = para.map(|p| p.props_at(0).clone()).unwrap_or_default();
let text_font = s.doc.styles.resolve_char_in(props.style.as_deref(), None, &first).font;
DropCapState {
position: match (rp.drop_cap, rp.drop_cap_margin) {
(0, _) => "none",
(_, true) => "margin",
_ => "dropped",
},
lines: if rp.drop_cap == 0 { 3 } else { rp.drop_cap },
distance: if rp.drop_cap == 0 { 0.0 } else { rp.drop_cap_distance },
font: rp.drop_cap_font.filter(|_| rp.drop_cap > 0),
text_font,
}
}
+9 -1
View File
@@ -125,7 +125,15 @@ pub fn specs() -> Vec<CommandSpec> {
// membership and drop cap stay. ⌘Q quits on macOS, so Word for Mac uses ⌃Q.
CommandSpec::new("para.reset", "Reset Paragraph Formatting", "Home › Paragraph", |s, _| {
fmt(s, &|p| {
*p = ParaProps { style: p.style.take(), numbering: p.numbering.take(), drop_cap: p.drop_cap.take(), ..Default::default() }
*p = ParaProps {
style: p.style.take(),
numbering: p.numbering.take(),
drop_cap: p.drop_cap.take(),
drop_cap_margin: p.drop_cap_margin.take(),
drop_cap_distance: p.drop_cap_distance.take(),
drop_cap_font: p.drop_cap_font.take(),
..Default::default()
}
})
})
.key(super::mac_or("Ctrl+Q", "Mod+Q")),
+45
View File
@@ -8325,3 +8325,48 @@ fn toc_keeps_direct_number_tabs() {
assert_eq!(entries, 2);
}
}
/// #2601: Insert › Drop Cap's menu and Drop Cap Options. Dropped and In margin take the
/// documented defaults (3 lines, distance 0, the text's font); the dialog's values (lines,
/// distance, font) are kept and clamped; None takes the drop cap away with its font; each is one
/// undo step and the caret stays where it was. The command is off where Word disables it: an
/// empty paragraph, a table cell, a paragraph that starts with a space.
#[test]
fn drop_cap_options_set_position_lines_distance_and_font() {
let props = |s: &Session| s.doc.body.first().and_then(|b| b.as_para()).map(|p| p.props.clone()).unwrap_or_default();
let cap = |s: &Session| {
let p = props(s);
(p.drop_cap, p.drop_cap_margin, p.drop_cap_distance, p.drop_cap_font)
};
let mut s = s();
assert!(s.run("insert.dropCap", &json!({})).is_err(), "off in an empty paragraph");
run(&mut s, "text.insert", json!({"text": "Once upon a time"}));
let caret = s.sel.clone();
run(&mut s, "insert.dropCap", json!({"position": "margin"}));
assert_eq!(cap(&s), (Some(3), Some(true), None, None));
assert_eq!(s.sel, caret, "the caret stays");
run(&mut s, "insert.dropCap", json!({"position": "dropped", "lines": 5, "distance": 7.2, "font": "Liberation Mono"}));
assert_eq!(cap(&s), (Some(5), None, Some(7.2), Some("Liberation Mono".to_string())));
run(&mut s, "edit.undo", json!({}));
assert_eq!(cap(&s), (Some(3), Some(true), None, None), "one undo step");
run(&mut s, "insert.dropCap", json!({"lines": 99, "distance": 1e30, "font": " "}));
assert_eq!(cap(&s), (Some(10), None, Some(wordcraft_doc::resolve::MAX_DROP_CAP_DISTANCE), None), "clamped");
run(&mut s, "insert.dropCap", json!({"lines": -4, "distance": -3, "position": "sideways"}));
assert_eq!(cap(&s), (Some(1), None, None, None), "junk falls back to the nearest valid value");
run(&mut s, "insert.dropCap", json!({"font": "Liberation Mono", "distance": 3}));
run(&mut s, "insert.dropCap", json!({"position": "none"}));
assert_eq!(cap(&s), (None, None, None, None), "None takes the letter's font back too");
assert!(props(&s).is_empty());
run(&mut s, "insert.dropCap", json!({"lines": 2}));
run(&mut s, "insert.dropCap", json!({"lines": 0}));
assert_eq!(cap(&s), (None, None, None, None), "0 lines is None, as before");
let mut s = self::s();
run(&mut s, "text.insert", json!({"text": " leading space"}));
assert!(s.run("insert.dropCap", &json!({})).is_err(), "off when the paragraph starts with a space");
let mut s = self::s();
run(&mut s, "insert.table", json!({"rows": 1, "cols": 1}));
run(&mut s, "text.insert", json!({"text": "Cell"}));
assert!(s.sel.focus.path.cell().is_some());
assert!(s.run("insert.dropCap", &json!({})).is_err(), "off in a table cell");
}
+13 -3
View File
@@ -347,7 +347,8 @@ pub struct ParaLayout {
pub notes: Vec<(usize, u32)>,
/// Proofing issues: (byte start, byte end, grammar?).
pub issues: Vec<(usize, usize, bool)>,
/// Drop cap: (clusters at the start that form it, lines it drops, width with its gap).
/// Drop cap: (clusters at the start that form it, lines it drops, the indent of those lines:
/// the letter's width and its distance from the text, or 0 in the margin).
pub drop_cap: Option<(usize, u8, f32)>,
/// Clusters after which the line may break with a hyphen (soft hyphens, auto hyphenation), sorted.
pub hyph_after: Vec<u32>,
@@ -1485,10 +1486,16 @@ pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout {
let mut big = (*rc).clone();
// `clamp` would panic here when the text itself is larger than 1000 pt.
big.size = (((lines - 1.0) * line_h + rc.size * 0.7) / 0.7).min(1000.0).max(rc.size);
if let Some(f) = &rp.drop_cap_font {
big.font = f.clone();
}
let before = b.clusters.len();
b.shape(first, 0, &Arc::new(big), None);
let w: f32 = b.clusters.get(before..).map(|c| c.iter().map(|c| c.adv).sum()).unwrap_or(0.0);
drop_cap = Some((b.clusters.len() - before, rp.drop_cap, w + rc.size * 0.3));
// Dropped: the first lines step aside by the letter and its distance from the text.
// In margin: they keep their indent and the letter hangs left of them.
let indent = if rp.drop_cap_margin { 0.0 } else { w + rp.drop_cap_distance };
drop_cap = Some((b.clusters.len() - before, rp.drop_cap, indent));
drop_cap_w = w;
// The letter hangs beside the text; lines flow at the indent `break_lines` adds.
for c in b.clusters.iter_mut().skip(before) {
@@ -1951,6 +1958,7 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u32, level: Optio
loop {
let mut lo = if first { first_left } else { rp.indent_left };
if let Some((_, dl, dw)) = pl.drop_cap
&& dw > 0.0
&& lines.len() < dl as usize
{
lo = first_left.max(rp.indent_left) + dw;
@@ -2482,7 +2490,9 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u32, level: Optio
{
let short = lines.len() < dl as usize;
let dy = if short { (dl as f32 - 1.0) * l0.height } else { ln.baseline - l0.baseline };
let dx = dw.min(first_x0.unwrap_or(0.0) - first_left.max(rp.indent_left));
let lead = first_x0.unwrap_or(0.0) - first_left.max(rp.indent_left);
// In margin, the letter's right edge stays the distance away from the text's edge.
let dx = if rp.drop_cap_margin { lead.max(0.0) + pl.drop_cap_w + rp.drop_cap_distance } else { dw.min(lead) };
// Toward the start edge: left, or right (and from the letter's right edge) when mirrored.
let dx = if rtl_para { pl.drop_cap_w - dx } else { dx };
if short {
+52
View File
@@ -2906,6 +2906,58 @@ fn drop_cap_indents_its_lines() {
assert!(g < -20.0, "dy {g}");
}
/// The first laid-out paragraph of a one-paragraph document with a drop cap set by `set`.
fn drop_cap_layout(set: impl Fn(&mut wordcraft_doc::props::ParaProps)) -> std::sync::Arc<crate::para::ParaLayout> {
let mut p =
wordcraft_doc::Paragraph::with_text(&"Every line of this paragraph wraps around a large first letter. ".repeat(6), Default::default());
p.props.drop_cap = Some(3);
set(&mut p.props);
let mut d = Document::from_text("x");
d.body = vec![wordcraft_doc::para_block(p)];
let l = lay(&d);
let Some(pl) = l.pages[0].items.iter().find_map(|i| if let Placed::Lines { para, .. } = i { Some(para.clone()) } else { None }) else {
panic!("no lines")
};
pl
}
/// #2601: Drop Cap Options. Dropped indents the first lines by the letter's width plus the
/// distance from the text; In margin leaves every line at the paragraph's indent and hangs the
/// letter left of it, the distance away; the letter takes the chosen font.
#[test]
fn drop_cap_options_place_the_letter() {
let x0 = |pl: &crate::para::ParaLayout, k: usize| pl.lines.get(k).and_then(|l| l.xs.first().copied()).unwrap_or(f32::NAN);
let letter_x = |pl: &crate::para::ParaLayout| x0(pl, 0) + pl.glyphs.first().map(|g| g.dx).unwrap_or(f32::NAN);
let plain = drop_cap_layout(|_| {});
let w = plain.drop_cap_w;
assert!(w > 20.0, "letter width {w}");
assert!((letter_x(&plain)).abs() < 0.5, "a dropped letter sits at the indent: {}", letter_x(&plain));
let far = drop_cap_layout(|p| p.drop_cap_distance = Some(18.0));
for k in 0..3 {
assert!((x0(&far, k) - (w + 18.0)).abs() < 0.5, "dropped line {k} at {} (letter {w} + 18)", x0(&far, k));
}
assert!(x0(&far, 3).abs() < 0.5, "line 4 is back at the indent: {}", x0(&far, 3));
assert!((letter_x(&far)).abs() < 0.5, "the letter stays at the indent: {}", letter_x(&far));
let margin = drop_cap_layout(|p| {
p.drop_cap_margin = Some(true);
p.drop_cap_distance = Some(9.0);
});
for k in 0..4 {
assert!(x0(&margin, k).abs() < 0.5, "in margin, line {k} is not indented: {}", x0(&margin, k));
}
assert!((letter_x(&margin) + margin.drop_cap_w + 9.0).abs() < 0.5, "the letter hangs left of the text: {}", letter_x(&margin));
assert!(margin.glyphs.first().is_some_and(|g| g.dy < -20.0), "still dropped toward the third line");
let font = drop_cap_layout(|p| p.drop_cap_font = Some("Liberation Mono".into()));
let cap_style = font.clusters.first().and_then(|c| font.styles.get(c.style as usize)).map(|s| s.rc.font.clone());
assert_eq!(cap_style.as_deref(), Some("Liberation Mono"));
let text_style = font.clusters.get(1).and_then(|c| font.styles.get(c.style as usize)).map(|s| s.rc.font.clone());
assert_ne!(text_style.as_deref(), Some("Liberation Mono"), "only the letter");
}
#[test]
fn auto_hyphenation_breaks_long_words() {
let text = "Internationalization considerations notwithstanding, administrators systematically reconsidered extraordinarily uncharacteristic responsibilities. ".repeat(4);
+139 -1
View File
@@ -20,6 +20,7 @@ pub enum InsertDialog {
SmartArt(crate::dialogs_smart_art::SmartArtForm),
ChartType(crate::dialogs_chart::ChartTypeForm),
ChartData(crate::dialogs_chart::ChartDataForm),
DropCap(DropCapForm),
}
impl InsertDialog {
@@ -32,6 +33,7 @@ impl InsertDialog {
InsertDialog::ChartType(f) if f.change => "changeChartType",
InsertDialog::ChartType(_) => "insertChart",
InsertDialog::ChartData(_) => "chartData",
InsertDialog::DropCap(_) => "dropCap",
}
}
@@ -45,13 +47,14 @@ impl InsertDialog {
InsertDialog::ChartType(f) if f.change => "Change Chart Type",
InsertDialog::ChartType(_) => "Insert Chart",
InsertDialog::ChartData(_) => "Edit Data",
InsertDialog::DropCap(_) => "Drop Cap",
}
}
}
/// The dialog `ui.dialog` opens by `name`: `columns`, `symbol`, `specialCharacters`, `field`,
/// `insertChart`, `changeChartType` or `chartData` (the last two for the selected chart), or
/// `insertSmartArt`.
/// `insertSmartArt`, or `dropCap` (Drop Cap Options…, where the paragraph can take one).
pub fn open(name: &str, app: &WordApp) -> Option<InsertDialog> {
Some(match name {
"columns" => InsertDialog::Columns(ColumnsForm::read(app)),
@@ -62,6 +65,7 @@ pub fn open(name: &str, app: &WordApp) -> Option<InsertDialog> {
"insertChart" => InsertDialog::ChartType(crate::dialogs_chart::ChartTypeForm::new(app, false)?),
"changeChartType" => InsertDialog::ChartType(crate::dialogs_chart::ChartTypeForm::new(app, true)?),
"chartData" => InsertDialog::ChartData(crate::dialogs_chart::ChartDataForm::new(app)?),
"dropCap" => InsertDialog::DropCap(DropCapForm::read(app)?),
_ => return None,
})
}
@@ -75,6 +79,7 @@ pub fn body(app: &mut WordApp, ui: &mut Ui, d: &mut InsertDialog) -> bool {
InsertDialog::SmartArt(f) => crate::dialogs_smart_art::picker_ui(app, ui, f),
InsertDialog::ChartType(f) => crate::dialogs_chart::type_ui(app, ui, f),
InsertDialog::ChartData(f) => crate::dialogs_chart::data_ui(app, ui, f),
InsertDialog::DropCap(f) => drop_cap_ui(app, ui, f),
}
}
@@ -97,6 +102,91 @@ pub(crate) fn buttons(ui: &mut Ui, ok: &str, cancel: &str, ok_enabled: bool) ->
r
}
// ---------------------------------------------------------------------------------------------
// Drop Cap (#2601)
/// The Drop Cap dialog's fields (Insert › Drop Cap › Drop Cap Options…). `distance` is in the
/// interface unit.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DropCapForm {
/// `none`, `dropped` or `margin`.
pub position: String,
/// The letter's font: its own, else the text's.
pub font: String,
/// The text's font: choosing it leaves the letter without a font of its own.
pub text_font: String,
pub lines: u32,
pub distance: f32,
}
impl DropCapForm {
/// The paragraph's drop cap; `None` where it can't take one (Word greys the button out).
pub fn read(app: &WordApp) -> Option<DropCapForm> {
if !crate::widgets::enabled(app, "insert.dropCap") {
return None;
}
let st = wordcraft_engine::cmd::insert::drop_cap_state(&app.session);
Some(DropCapForm {
position: st.position.into(),
font: st.font.clone().unwrap_or_else(|| st.text_font.clone()),
text_font: st.text_font,
lines: u32::from(st.lines.clamp(1, 10)),
distance: st.distance.max(0.0) / crate::options::unit().pt_per_unit(),
})
}
/// `insert.dropCap` parameters.
pub fn params(&self) -> Value {
if self.position == "none" {
return json!({"position": "none"});
}
let font = Some(self.font.trim()).filter(|f| !f.is_empty() && *f != self.text_font);
json!({
"position": self.position,
"lines": self.lines.clamp(1, 10),
"distance": (self.distance * crate::options::unit().pt_per_unit()).max(0.0),
"font": font,
})
}
}
fn drop_cap_ui(app: &mut WordApp, ui: &mut Ui, f: &mut DropCapForm) -> bool {
ui.label(egui::RichText::new(tl!("Position")).font(semibold(12.5)));
ui.horizontal(|ui| {
for (v, l) in [("none", "None"), ("dropped", "Dropped"), ("margin", "In margin")] {
ui.radio_value(&mut f.position, v.to_string(), tl!(l));
}
});
ui.add_space(6.0);
ui.label(egui::RichText::new(tl!("Options")).font(semibold(12.5)));
let fams = app.previews.families();
let preview = app.previews.font_preview_fn();
let unit = crate::options::unit();
ui.add_enabled_ui(f.position != "none", |ui| {
egui::Grid::new("dropcap").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Font:"));
if let Some(v) = crate::widgets::font_combo(ui, "dropcap_font", 190.0, &f.font, &fams, Some(&*preview))
&& v != crate::widgets::ADD_FONT
{
f.font = v;
}
ui.end_row();
ui.label(tl!("Lines to drop:"));
ui.add(egui::DragValue::new(&mut f.lines).range(1..=10));
ui.end_row();
ui.label(tl!("Distance from text:"));
ui.add(egui::DragValue::new(&mut f.distance).speed(0.01).range(0.0..=22.0 * 72.0 / unit.pt_per_unit()).measure(unit).max_decimals(2));
ui.end_row();
});
});
let (ok, cancel) = buttons(ui, "OK", "Cancel", true);
if ok {
let _ = app.run("insert.dropCap", f.params());
}
ok || cancel
}
// ---------------------------------------------------------------------------------------------
// Columns
@@ -1340,6 +1430,54 @@ mod tests {
WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("one\ntwo")), Default::default())
}
fn drop_cap_form(a: &mut WordApp) -> Option<DropCapForm> {
a.run("ui.dialog", json!({"name": "dropCap"})).unwrap();
match a.dialog.take() {
Some(crate::dialogs::Dialog::Insert(d)) => match *d {
InsertDialog::DropCap(f) => Some(f),
d => panic!("not drop cap: {d:?}"),
},
_ => None,
}
}
/// #2601: Drop Cap Options… opens on the paragraph's drop cap (the defaults without one:
/// 3 lines, distance 0, the text's font) in the interface unit, and OK runs `insert.dropCap`
/// with the chosen values; it reopens showing them. Choosing the text's own font sends none.
#[test]
fn drop_cap_dialog_prefills_and_applies() {
let mut a = app();
a.run("file.options", json!({"units": "centimeters"})).unwrap();
let mut f = drop_cap_form(&mut a).expect("the dialog");
assert_eq!((f.position.as_str(), f.lines, f.distance), ("none", 3, 0.0));
assert_eq!(f.font, f.text_font, "the text's font");
assert!(!f.font.is_empty());
assert_eq!(f.params(), json!({"position": "none"}));
f.position = "margin".into();
f.lines = 4;
f.distance = 1.0;
a.run("insert.dropCap", f.params()).unwrap();
let props = |a: &WordApp| a.session.doc.body.first().and_then(|b| b.as_para()).map(|p| p.props.clone()).unwrap_or_default();
let p = props(&a);
assert_eq!((p.drop_cap, p.drop_cap_margin, p.drop_cap_font.as_deref()), (Some(4), Some(true), None));
assert!((p.drop_cap_distance.unwrap_or_default() - wordcraft_geom::PT_PER_CM).abs() < 0.01, "1 cm: {:?}", p.drop_cap_distance);
let mut f = drop_cap_form(&mut a).expect("the dialog");
assert_eq!((f.position.as_str(), f.lines), ("margin", 4));
assert!((f.distance - 1.0).abs() < 0.01, "{} cm", f.distance);
f.position = "dropped".into();
f.font = "Liberation Mono".into();
a.run("insert.dropCap", f.params()).unwrap();
let p = props(&a);
assert_eq!((p.drop_cap, p.drop_cap_margin, p.drop_cap_font.as_deref()), (Some(4), None, Some("Liberation Mono")));
assert_eq!(drop_cap_form(&mut a).map(|f| f.font), Some("Liberation Mono".to_string()));
assert_eq!(wordcraft_engine::cmd::insert::drop_cap_state(&a.session).position, "dropped", "the menu checks Dropped");
// Where Word greys Drop Cap out, there is no dialog.
a.run("caret.docEnd", json!({})).unwrap();
a.run("text.newParagraph", json!({})).unwrap();
assert!(drop_cap_form(&mut a).is_none(), "an empty paragraph");
}
/// More Columns: two unequal columns with a line between apply as one undo step.
#[test]
fn columns_dialog_applies_unequal_columns_in_one_step() {
+12
View File
@@ -2662,3 +2662,15 @@
text boxes can't go inside a text box текстови полета не могат да са в текстово поле
the cursor isn't in a table курсорът не е в таблица
{name} options {name}: опции
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Вградена
In margin В полето
Drop Cap Options… Опции за главна буква…
Lines to drop: Редове на буквата:
Distance from text: Разстояние от текста:
Distance from text Разстояние от текста
# Drop Cap disabled reasons (#2601)
the paragraph is empty абзацът е празен
the paragraph starts with a space абзацът започва с интервал
Can't render this file because it contains an unexpected character in line 7 and column 25.
+12
View File
@@ -2665,3 +2665,15 @@ preview Title Ukázka
text boxes can't go inside a text box textové pole nelze vložit do textového pole
the cursor isn't in a table kurzor není v tabulce
{name} options Možnosti: {name}
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Zapuštěná
In margin Na okraji
Drop Cap Options… Možnosti iniciály…
Lines to drop: Výška v řádcích:
Distance from text: Vzdálenost od textu:
Distance from text Vzdálenost od textu
# Drop Cap disabled reasons (#2601)
the paragraph is empty odstavec je prázdný
the paragraph starts with a space odstavec začíná mezerou
Can't render this file because it contains an unexpected character in line 7 and column 25.
+8
View File
@@ -2266,3 +2266,11 @@
{added}; {count} couldn't be read: {names} {added}; {count} nicht lesbar: {names}
{names} and {n} more {names} und {n} weitere
{added}; {n} skipped (limit {max}) {added}; {n} übersprungen (Limit {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Im Text
In margin Im Rand
Drop Cap Options… Initialoptionen…
Lines to drop: Initialhöhe (Zeilen):
Distance from text: Abstand zum Text:
Distance from text Abstand zum Text
Can't render this file because it contains an unexpected character in line 6 and column 25.
+8
View File
@@ -2633,3 +2633,11 @@ preview Title Título
{added}; {count} couldn't be read: {names} {added}; no se pudieron leer {count}: {names}
{names} and {n} more {names} y {n} más
{added}; {n} skipped (limit {max}) {added}; {n} omitidas (límite {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped En texto
In margin En margen
Drop Cap Options… Opciones de letra capital…
Lines to drop: Líneas que ocupa:
Distance from text: Distancia del texto:
Distance from text Distancia del texto
Can't render this file because it contains an unexpected character in line 6 and column 25.
+8
View File
@@ -2301,3 +2301,11 @@
{added}; {count} couldn't be read: {names} {added}; {count} ei õnnestunud lugeda: {names}
{names} and {n} more {names} ja veel {n}
{added}; {n} skipped (limit {max}) {added}; {n} jäeti vahele (piir {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Tekstis
In margin Veerisel
Drop Cap Options… Süvisinitsiaali suvandid…
Lines to drop: Ridade arv:
Distance from text: Kaugus tekstist:
Distance from text Kaugus tekstist
Can't render this file because it contains an unexpected character in line 847 and column 23.
+8
View File
@@ -2679,3 +2679,11 @@
✓ None ✓ Aucun
preview Title Titre
template Letter Lettre
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Dans le texte
In margin Dans la marge
Drop Cap Options… Options de lettrine…
Lines to drop: Hauteur (lignes) :
Distance from text: Distance du texte :
Distance from text Distance du texte
Can't render this file because it contains an unexpected character in line 7 and column 25.
+12
View File
@@ -2673,3 +2673,15 @@ preview Title Naslov
text boxes can't go inside a text box tekstni okviri ne mogu biti unutar tekstnog okvira
the cursor isn't in a table kursor nije u tablici
{name} options Mogućnosti: {name}
# Drop Cap menu and Drop Cap Options (#2601)
Dropped U tekstu
In margin Na margini
Drop Cap Options… Mogućnosti inicijala…
Lines to drop: Broj redaka:
Distance from text: Udaljenost od teksta:
Distance from text Udaljenost od teksta
# Drop Cap disabled reasons (#2601)
the paragraph is empty odlomak je prazan
the paragraph starts with a space odlomak počinje razmakom
Can't render this file because it contains an unexpected character in line 6 and column 25.
+12
View File
@@ -2669,3 +2669,15 @@
text boxes can't go inside a text box szövegdoboz nem kerülhet szövegdobozba
the cursor isn't in a table a kurzor nem táblázatban van
{name} options {name} beállításai
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Süllyesztett
In margin Margón
Drop Cap Options… Iniciálé beállításai…
Lines to drop: Sorok száma:
Distance from text: Távolság a szövegtől:
Distance from text Távolság a szövegtől
# Drop Cap disabled reasons (#2601)
the paragraph is empty a bekezdés üres
the paragraph starts with a space a bekezdés szóközzel kezdődik
Can't render this file because it contains an unexpected character in line 6 and column 25.
+12
View File
@@ -2682,3 +2682,15 @@ preview Title Esempio
text boxes can't go inside a text box le caselle di testo non possono stare in una casella di testo
the cursor isn't in a table il cursore non è in una tabella
{name} options Opzioni di {name}
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Nel testo
In margin Nel margine
Drop Cap Options… Opzioni capolettera…
Lines to drop: Altezza in righe:
Distance from text: Distanza dal testo:
Distance from text Distanza dal testo
# Drop Cap disabled reasons (#2601)
the paragraph is empty il paragrafo è vuoto
the paragraph starts with a space il paragrafo inizia con uno spazio
Can't render this file because it contains an unexpected character in line 6 and column 25.
+8
View File
@@ -2267,3 +2267,11 @@
{added}; {count} couldn't be read: {names} {added}。読み込めなかったもの {count} 件: {names}
{names} and {n} more {names} ほか {n} 件
{added}; {n} skipped (limit {max}) {added}。{n} 件をスキップしました (上限 {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped 本文内に表示
In margin 余白に表示
Drop Cap Options… ドロップ キャップのオプション…
Lines to drop: ドロップする行数:
Distance from text: 本文からの距離:
Distance from text 本文からの距離
Can't render this file because it contains an unexpected character in line 925 and column 23.
+8
View File
@@ -2520,3 +2520,11 @@ preview Title 제목
{added}; {count} couldn't be read: {names} {added}; 읽을 수 없는 파일 {count}개: {names}
{names} and {n} more {names} 외 {n}개
{added}; {n} skipped (limit {max}) {added}; {n}개 건너뜀 (한도 {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped 본문 안
In margin 여백에
Drop Cap Options… 문단 첫 글자 장식 옵션…
Lines to drop: 장식할 줄 수:
Distance from text: 본문과의 간격:
Distance from text 본문과의 간격
Can't render this file because it contains an unexpected character in line 931 and column 23.
+12
View File
@@ -2678,3 +2678,15 @@ preview Title Tytuł
text boxes can't go inside a text box pola tekstowego nie można umieścić w polu tekstowym
the cursor isn't in a table kursor nie jest w tabeli
{name} options {name}: opcje
# Drop Cap menu and Drop Cap Options (#2601)
Dropped W tekście
In margin Na marginesie
Drop Cap Options… Opcje inicjału…
Lines to drop: Wysokość w wierszach:
Distance from text: Odległość od tekstu:
Distance from text Odległość od tekstu
# Drop Cap disabled reasons (#2601)
the paragraph is empty akapit jest pusty
the paragraph starts with a space akapit zaczyna się od spacji
Can't render this file because it contains an unexpected character in line 6 and column 25.
+8
View File
@@ -2279,3 +2279,11 @@
{added}; {count} couldn't be read: {names} {added}; {count} não puderam ser lidas: {names}
{names} and {n} more {names} e mais {n}
{added}; {n} skipped (limit {max}) {added}; {n} ignoradas (limite de {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped No texto
In margin Na margem
Drop Cap Options… Opções de capitular…
Lines to drop: Linhas ocupadas:
Distance from text: Distância do texto:
Distance from text Distância do texto
Can't render this file because it contains an unexpected character in line 6 and column 25.
+12
View File
@@ -2671,3 +2671,15 @@ preview Title Пример
text boxes can't go inside a text box надпись нельзя поместить внутрь надписи
the cursor isn't in a table курсор не в таблице
{name} options {name}: параметры
# Drop Cap menu and Drop Cap Options (#2601)
Dropped В тексте
In margin На поле
Drop Cap Options… Параметры буквицы…
Lines to drop: Высота в строках:
Distance from text: Расстояние от текста:
Distance from text Расстояние от текста
# Drop Cap disabled reasons (#2601)
the paragraph is empty абзац пуст
the paragraph starts with a space абзац начинается с пробела
Can't render this file because it contains an unexpected character in line 7 and column 25.
+8
View File
@@ -2277,3 +2277,11 @@
{added}; {count} couldn't be read: {names} {added}; nije moguće pročitati ({count}): {names}
{names} and {n} more {names} i još {n}
{added}; {n} skipped (limit {max}) {added}; preskočeno: {n} (ograničenje {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped U tekstu
In margin Na margini
Drop Cap Options… Opcije ukrasnog slova…
Lines to drop: Broj redova:
Distance from text: Rastojanje od teksta:
Distance from text Rastojanje od teksta
Can't render this file because it contains an unexpected character in line 6 and column 25.
+8
View File
@@ -2281,3 +2281,11 @@
{added}; {count} couldn't be read: {names} {added}; није могуће прочитати ({count}): {names}
{names} and {n} more {names} и још {n}
{added}; {n} skipped (limit {max}) {added}; прескочено: {n} (ограничење {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped У тексту
In margin На маргини
Drop Cap Options… Опције украсног слова…
Lines to drop: Број редова:
Distance from text: Растојање од текста:
Distance from text Растојање од текста
Can't render this file because it contains an unexpected character in line 11 and column 25.
+12
View File
@@ -2688,3 +2688,15 @@ preview Title Titel
text boxes can't go inside a text box textrutor kan inte placeras i en textruta
the cursor isn't in a table markören är inte i en tabell
{name} options Alternativ för {name}
# Drop Cap menu and Drop Cap Options (#2601)
Dropped I texten
In margin I marginalen
Drop Cap Options… Alternativ för anfang…
Lines to drop: Antal rader:
Distance from text: Avstånd från text:
Distance from text Avstånd från text
# Drop Cap disabled reasons (#2601)
the paragraph is empty stycket är tomt
the paragraph starts with a space stycket börjar med ett blanksteg
Can't render this file because it contains an unexpected character in line 7 and column 25.
+12
View File
@@ -2670,3 +2670,15 @@ preview Title Örnek
text boxes can't go inside a text box metin kutuları bir metin kutusunun içine konamaz
the cursor isn't in a table imleç bir tabloda değil
{name} options {name} seçenekleri
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Metin içinde
In margin Kenar boşluğunda
Drop Cap Options… Büyük başlangıç harfi seçenekleri…
Lines to drop: Kaplayacağı satır:
Distance from text: Metinden uzaklık:
Distance from text Metinden uzaklık
# Drop Cap disabled reasons (#2601)
the paragraph is empty paragraf boş
the paragraph starts with a space paragraf boşlukla başlıyor
Can't render this file because it contains an unexpected character in line 799 and column 23.
+8
View File
@@ -2279,3 +2279,11 @@
{added}; {count} couldn't be read: {names} {added}; не вдалося прочитати ({count}): {names}
{names} and {n} more {names} та ще {n}
{added}; {n} skipped (limit {max}) {added}; пропущено: {n} (ліміт {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped У тексті
In margin На полі
Drop Cap Options… Параметри буквиці…
Lines to drop: Висота в рядках:
Distance from text: Відстань від тексту:
Distance from text Відстань від тексту
Can't render this file because it contains an unexpected character in line 6 and column 31.
+12
View File
@@ -2713,3 +2713,15 @@ template Letter Thư
text boxes can't go inside a text box không thể đặt hộp văn bản vào trong hộp văn bản
the cursor isn't in a table con trỏ không nằm trong bảng
{name} options Tùy chọn của {name}
# Drop Cap menu and Drop Cap Options (#2601)
Dropped Trong văn bản
In margin Trong lề
Drop Cap Options… Tùy chọn chữ lớn đầu đoạn…
Lines to drop: Số dòng chiếm:
Distance from text: Khoảng cách tới văn bản:
Distance from text Khoảng cách tới văn bản
# Drop Cap disabled reasons (#2601)
the paragraph is empty đoạn văn trống
the paragraph starts with a space đoạn văn bắt đầu bằng dấu cách
Can't render this file because it contains an unexpected character in line 7 and column 25.
+8
View File
@@ -2279,3 +2279,11 @@
{added}; {count} couldn't be read: {names} {added};{count} 个无法读取:{names}
{names} and {n} more {names} 等另外 {n} 个
{added}; {n} skipped (limit {max}) {added};已跳过 {n} 个(上限 {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped 下沉
In margin 悬挂
Drop Cap Options… 首字下沉选项…
Lines to drop: 下沉行数:
Distance from text: 距正文:
Distance from text 距正文
Can't render this file because it contains an unexpected character in line 7 and column 25.
+8
View File
@@ -2268,3 +2268,11 @@
{added}; {count} couldn't be read: {names} {added};{count} 個無法讀取:{names}
{names} and {n} more {names} 等另外 {n} 個
{added}; {n} skipped (limit {max}) {added};已略過 {n} 個(上限 {max})
# Drop Cap menu and Drop Cap Options (#2601)
Dropped 下沉
In margin 懸掛
Drop Cap Options… 首字放大選項…
Lines to drop: 放大行數:
Distance from text: 與本文距離:
Distance from text 與本文距離
Can't render this file because it contains an unexpected character in line 926 and column 23.
+1 -1
View File
@@ -201,7 +201,7 @@ fn controls(tab: &str) -> Vec<Control> {
c("Text Box", "insert.textBox"),
c("Quick Parts", "insert.quickParts"),
m("WordArt", "insert.wordArt"),
c("Drop Cap", "insert.dropCap"),
m("Drop Cap", "insert.dropCap"),
c("Signature Line", "insert.signatureLine"),
c("Date & Time", "insert.dateTime"),
c("Object", "insert.object"),
+4
View File
@@ -659,6 +659,9 @@ fn describe(prop: &str, v: &Value) -> String {
"suppressLineNumbers" => "Suppress line numbers",
"bidi" => "Right-to-left",
"dropCap" => "Drop cap",
"dropCapMargin" => "In margin",
"dropCapDistance" => "Distance from text",
"dropCapFont" => "Font",
"style" => "Style",
"numbering" => "Numbering",
"lang" => "Language",
@@ -679,6 +682,7 @@ fn describe(prop: &str, v: &Value) -> String {
l @ 0..=8 => crate::i18n::fmt(tl!("Level {n}"), &[("n", &(l + 1).to_string())]),
_ => tl!("Body Text").to_string(),
},
("dropCapFont", Value::String(s)) => s.clone(),
("dropCap", Value::Number(n)) => crate::i18n::fmt(tl!("{n} lines"), &[("n", &n.to_string())]),
(_, Value::Number(n)) => pt(n.as_f64().unwrap_or(0.0)),
("align", Value::String(s)) => tl!(match s.as_str() {
+12 -1
View File
@@ -1304,7 +1304,7 @@ fn insert(app: &mut WordApp, ui: &mut Ui) {
menu_button(ui, app, "wordArt", Some("WordArt"), "WordArt", false, "", |ui, app| {
crate::wordart_menus::style_gallery(ui, app, "insert.wordArt")
});
small(ui, app, "dropCap", Some("Drop Cap"), "Drop Cap", "insert.dropCap", json!({}), false);
menu_button(ui, app, "dropCap", Some("Drop Cap"), "Drop Cap", false, "insert.dropCap", drop_cap_items);
});
stack(ui, |ui| {
small(ui, app, "signature", None, "Signature Line", "insert.signatureLine", json!({}), false);
@@ -1348,6 +1348,17 @@ fn insert(app: &mut WordApp, ui: &mut Ui) {
});
}
/// Insert › Drop Cap's menu (#2601): None, Dropped and In margin (the current one checked),
/// then the Drop Cap dialog.
fn drop_cap_items(ui: &mut Ui, app: &mut WordApp) {
let current = wordcraft_engine::cmd::insert::drop_cap_state(&app.session).position;
for (label, v) in [("None", "none"), ("Dropped", "dropped"), ("In margin", "margin")] {
mi_check(ui, app, label, current == v, "insert.dropCap", json!({"position": v}));
}
ui.separator();
mi(ui, app, "Drop Cap Options…", "ui.dialog", json!({"name": "dropCap"}));
}
fn draw(app: &mut WordApp, ui: &mut Ui) {
use wordcraft_doc::freeform::InkTool;
use wordcraft_engine::cmd::draw::{DrawMode, lasso_selection};
+2 -1
View File
@@ -38,7 +38,7 @@ Active footnote and endnote text stays readable while editing (#2545). This fixe
| 2 | **Word's native ink is not drawn from its strokes**; OLE objects show only their picture | B | Every native ink form is preserved: drawing-level ink with its optional picture fallback, direct run-level content parts, and ink members of groups and drawing canvases (kept in their place among the members, drawn from their fallback picture). Drawing canvases are read and written (#344); group shapes (`wpg`) read and written (#267). Charts and SmartArt are drawn (#292) and, with OLE objects, written back with all their parts on save (#319) | Pen notes from Word show only as their picture and can't be edited; OLE objects can't be opened | 45–70 to render and edit charts and SmartArt (#13) | [file-format-parity.md](file-format-parity.md) |
| 5 | **Content controls** landed (#381): `w:sdt` kept, shown and edited (Developer tab, Design Mode, Properties). Move tracking landed (#400): `w:moveFrom`/`w:moveTo` with their named ranges round-trip, a tracked cut and paste of a paragraph or sentence records a move, either end accepts/rejects both, green double strike/underline markup and "Moved from/to" balloons; Track Changes Options has the Moves rows (Moved from / Moved to mark and colour) and `w:doNotTrackMoves` (Track moves off: a cut and paste is a deletion and an insertion) round-trips; still missing: drag-and-drop moves (no text drag yet). Formatting revisions landed (#41): `w:rPrChange`/`w:pPrChange`/`w:tblPrChange`/`w:trPrChange`/`w:tcPrChange`/`w:sectPrChange`/`w:numberingChange` round-trip, are recorded while tracking, accepted/rejected and shown in balloons; still missing: tracked style-definition changes and table/section changes made in WordCraft. Opaque custom XML backing stores now survive saves; binding evaluation remains absent. Replace All now checks content-control locks before editing any match; a refused batch preserves the document and undo history. | B | List-label pitch corrected in synthetic metric controls; Issues #41, #400; source grep | Legal and editorial review workflows; templates with form controls | 5–8 | [file-format-parity.md](file-format-parity.md) |
| 6 | **Print dialog depth**: desktop printer choice, copies, collation, duplex and grayscale submit through craft-print; File › Print retains preview, page ranges and odd/even (#641); driver tray/options and pages per sheet remain; web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) |
| 7 | **Dialog depth**: 58 modal dialogs vs Word's ~100 (Split Cells and Convert Text to Table match Word's observed limits and defaults) | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs`; Cell Margins (#1960) now opens with current defaults and applies only confirmed edits | Power users reach for dialogs the ribbon alone doesn't cover | 11–19 | [ui-parity.md](ui-parity.md) |
| 7 | **Dialog depth**: 59 modal dialogs vs Word's ~100 (Drop Cap Options matches Word's menu and dialog; Split Cells and Convert Text to Table match Word's observed limits and defaults) | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs`; Cell Margins (#1960) now opens with current defaults and applies only confirmed edits | Power users reach for dialogs the ribbon alone doesn't cover | 11–19 | [ui-parity.md](ui-parity.md) |
| 9 | **Password-protected `.doc` files** (AES) can't be opened; password-protected `.docx` opens and saves (agile encryption, #55 closed by #281) | | `crates/docx/src/crypt.rs`; `crates/docbin/src/lib.rs` opens XOR/RC4/RC4 CryptoAPI `.doc` with a password (#1079, untested on Word-written files; AES `.doc` refused) | Corporate and legal users with old files | 3–5 | [file-format-parity.md](file-format-parity.md) |
| 8 | **Proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech and Indonesian only**; no dictionaries, grammar or hyphenation for the other ~42 languages Word proofs (text in them is left unmarked) | | `crates/proof` `ProofLang::PROOFED`: English, Polish, Portuguese (pt-BR via the installed VERO dictionary, #797), Italian (it-IT via the installed Hunspell dictionary), Croatian (hr-HR, 2026-10-13), Turkish (tr_TR spelling, grammar, TDK hyphenation, 2026-10-13) Russian (the installed `ru_RU` dictionary, 2026-10-12) and Indonesian (the installed `id_ID` dictionary, #498, 2026-10-16), a language per run; Bulgarian has hyphenation only (#826); open PRs add Danish (#723), Russian (#894) and Czech (#1048); Word ships 120 proofing tools; issues #25, #40, #100 open | Every writer outside English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech and Indonesian | 20–40 | [localization-parity.md](localization-parity.md) |
| 10 | **Objects**: text in a freely rotated text box (free rotation and flips landed, #332; text direction, alignment and linked text boxes landed, #369; contour wrap and wrap side landed, #354), shape effects, WordArt 3-D effects and connector routing around shapes (WordArt effects and warps and glued connectors landed, #416), dragging members inside a Drawing Canvas (the canvas landed, #344) | | [parity-checklist.md](parity-checklist.md) (Shape Format 11/11, Layout 22/22); group and ungroup landed (#267) | Newsletters, flyers, reports with diagrams | 20–30 | [target-app-parity.md](target-app-parity.md) |
@@ -125,6 +125,7 @@ The Language dialog reads effective character formatting, including document def
| 2026-10-15 | trivial | #2 impact restated: Word ink is no longer lost; it shows as its picture and can't be edited |
| 2026-10-15 | trivial | #2 narrowed: native ink in groups and drawing canvases (`w14:contentPart` members, with their picture fallback) survives DOCX edits and repeated saves; native ink rendering remains |
| 2026-10-16 | trivial | Body and active note text keep normal opacity while editing footnotes or endnotes (#2545); header/footer dimming is unchanged |
| 2026-10-18 | minor | #7: Drop Cap Options dialog (#2601), 59 modal dialogs |
| 2026-10-14 | trivial | Row table border exceptions fixed; other row table exceptions remain unsupported |
| 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks |
| 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged |
+2 -1
View File
@@ -47,7 +47,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic
| Widow/orphan control, keep with next, keep lines together, page break before | ✅ | `lib.rs` keep_next/keep_lines; a line holding only a page break stays at the bottom of a full page instead of leaving a blank page (#534); a paragraph ending with a page break keeps its space after on the old page, so the next page starts at the top (compatibility modes 12 to 15, `layout::tests::space_before_at_the_top_of_a_page_like_word`); a three-line paragraph with room for two moves whole (neither a widow nor an orphan is allowed), as in Word | — |
| Tabs (left, center, right, decimal, bar), leaders | 🟡 | Bar tabs untested. Positional tabs (`w:ptab`, ECMA-376 §17.3.3.23) go to the left edge, centre or right edge of the margins or indents whatever the tab stops, and to the next line when the text has passed that place (#2367, `layout::tests::right_positional_tab_ends_text_on_the_right_margin` and the three after it); not compared with Word on lines narrowed by floating objects or right-to-left paragraphs | 1–2 |
| Hyphenation (automatic, zone from the document, words in capitals, soft hyphens, consecutive limit, Manual Hyphenation, #407) | 🟡 | English patterns only; other languages hyphenate wrongly or not at all | in localization |
| Drop caps | ✅ | 2026-10-06 | — |
| Drop caps (dropped and in margin, lines, distance, font; `w:framePr w:dropCap`) | ✅ | 2026-10-17 | — |
| All Caps and Small Caps: the full capital of each letter, so ß draws as SS (ff as FF); the capitals drawn for one character are one cluster, so the caret, selection, hit testing and line breaking treat ß as one character | ✅ | `layout::tests::caps_draw_sharp_s_as_two_capitals_in_one_cluster` (#1581). The PDF text layer still reads the document's lowercase letters until #1473 | — |
| Columns, column breaks, separators, balancing | ✅ | Columns balance on a section's last page when the next section starts continuous (also at the document end, which Word only balances when the document ends with a continuous break); not after next-page/even/odd breaks or on a page with a column break; keep-together, keep-with-next, widow/orphan control and unsplittable rows can leave them slightly uneven (#350) | — |
| Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42). A break inserted with text selected goes before the selection and keeps it, as in Word (#852) | 1–2 |
@@ -231,6 +231,7 @@ Footer flow-table strokes reserve their drawn top edge, including the half-width
| 2026-10-16 | trivial | Subpoint grid units retain their declared widths without shrinking adjacent cells; tiny totals normalize without scale-factor overflow |
| 2026-10-18 | trivial | Any VML float in a header or footer cell is placed on the page, as Word for Mac does (user-checked, #1775) |
| 2026-10-16 | trivial | A header or footer cell's float placed relative to the page and not laid out in its cell (a picture watermark in a header that starts with a table) is placed on the page, behind the text (#1775) |
| 2026-10-17 | minor | Drop caps: In margin (letter in the left margin, text not indented), distance from text and the letter's font, round-tripped as `w:framePr` (#2601) |
| 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows |
| 2026-10-14 | trivial | Consistent cell widths override a stale table grid |
| 2026-10-14 | trivial | Paragraphs inside complex-field instructions take no flow height; empty result paragraphs keep their line. |
+2 -1
View File
@@ -26,7 +26,7 @@ Document title opens Rename Document; the account badge opens Options › Genera
| Modal dialogs | **55** (`Dialog` enum: Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Templates and Add-ins, Organizer, Compare and Combine, AutoCorrect Options, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402); Cross-reference (#916); Line Numbers, Hyphenation with Manual Hyphenation, Language, Envelopes, Labels (#407); Page Setup has a Paper size list and custom width and height, opened from Layout › Size › More Paper Sizes… (#1433)) plus panes and menus for the rest | ~100 (Options panes…) | ~45% | 11–19 |
| Keyboard shortcuts | **152** commands bound in `CommandSpec::key`, 172 key combinations (counted from the registry on Linux, 2026-10-13); styles, spacing, symbols, fields, views, mail merge (#330); field keys Alt+F9, Shift+F9, Ctrl+Shift+F9, Ctrl+F11, Ctrl+Shift+F11, Shift+F11 (#339), Next Field on Alt+F1 and Alt+Shift+F1 (Word's other keys for it) since F11 went to full screen (#980; GNOME and KDE may keep Alt+F1 for their own overview or application menu, and then Next Field has no key there; Previous Field keeps Shift+F11); Alt+Shift+arrows (move paragraphs, promote/demote), Shift+F5 Go Back and Alt+X (#343); Outline view: Alt+Shift+Plus/Minus expand/collapse, Alt+Shift+1–9 and Alt+Shift+A Show Level (#493); Alt+Q to "Tell me" command search (#819, not on macOS). Missing: en dash on the numeric keypad | ~250 default shortcuts | ~70% | 2–4 |
| Keyboard shortcuts | **152** commands bound in `CommandSpec::key`, 172 key combinations (counted from the registry on Linux, 2026-10-13); styles, spacing, symbols, fields, views, mail merge (#330); field keys Alt+F9, Shift+F9, Ctrl+Shift+F9, Ctrl+F11, Ctrl+Shift+F11, Shift+F11 (#339), Next Field on Alt+F1 and Alt+Shift+F1 (Word's other keys for it) since F11 went to full screen (#980; GNOME and KDE may keep Alt+F1 for their own overview or application menu, and then Next Field has no key there; Previous Field keeps Shift+F11); Alt+Shift+arrows (move paragraphs, promote/demote), Shift+F5 Go Back and Alt+X (#343); Outline view: Alt+Shift+Plus/Minus expand/collapse, Alt+Shift+1–9 and Alt+Shift+A Show Level (#493); Alt+Q to "Tell me" command search (#819, not on macOS); on macOS, Word for Mac's own keys: ⌘⇧S Save As, ⌘⌥E Insert Endnote, ⌘⌥A New Comment, ⌃H Replace, ⌘⌥R collapse the ribbon. The Spike: Ctrl+F3 cuts to it, Ctrl+Shift+F3 inserts it and empties it (⌘F3, ⌘⇧F3 on macOS). Missing: en dash on the numeric keypad Insert Hyperlink has Word's Link to: Existing File or Web Page / Place in This Document (top of the document, headings, bookmarks) (#1234). | ~250 default shortcuts | ~70% | 2–4 |
| Modal dialogs | **58** (`Dialog` enum: Discard Recovery, Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Templates and Add-ins, Organizer, Compare and Combine, AutoCorrect Options, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402); Cross-reference (#916); Line Numbers, Hyphenation with Manual Hyphenation, Language, Envelopes, Labels (#407); Page Setup has a Paper size list and custom width and height, opened from Layout › Size › More Paper Sizes… (#1433); every dialog's close control is a box at the title bar's right end, the full title-bar height and 1.6× as wide, all of it clickable, red with a white ✕ on hover as a Windows caption button (#1885); Split Cells (columns, rows, merge before split; defaults follow the selection as in Word: twice its columns, its rows, Merge offered for several cells or one merged across columns; a merged cell splits into 1 to its height rows, its height or 1 × 1 cutting it back into rows, more refused with Word's message; parts split across columns are equal and other rows keep their grid lines) and Convert Text to Table (live column count, read-only rows; Initial column width Auto or 0 to 22 in, AutoFit to contents/window; Paragraphs/Commas/Tabs/Other "-" separators) (#2592)) plus panes and menus for the rest | ~100 (Options panes…) | ~45% | 11–19 |
| Modal dialogs | **59** (`Dialog` enum: Discard Recovery, Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Templates and Add-ins, Organizer, Compare and Combine, AutoCorrect Options, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402); Cross-reference (#916); Drop Cap with Position, Font, Lines to drop and Distance from text, opened from Insert › Drop Cap › Drop Cap Options… (#2601); Line Numbers, Hyphenation with Manual Hyphenation, Language, Envelopes, Labels (#407); Page Setup has a Paper size list and custom width and height, opened from Layout › Size › More Paper Sizes… (#1433); every dialog's close control is a box at the title bar's right end, the full title-bar height and 1.6× as wide, all of it clickable, red with a white ✕ on hover as a Windows caption button (#1885); Split Cells (columns, rows, merge before split; defaults follow the selection as in Word: twice its columns, its rows, Merge offered for several cells or one merged across columns; a merged cell splits into 1 to its height rows, its height or 1 × 1 cutting it back into rows, more refused with Word's message; parts split across columns are equal and other rows keep their grid lines) and Convert Text to Table (live column count, read-only rows; Initial column width Auto or 0 to 22 in, AutoFit to contents/window; Paragraphs/Commas/Tabs/Other "-" separators) (#2592)) plus panes and menus for the rest | ~100 (Options panes…) | ~45% | 11–19 |
| Task panes (Navigation, Styles, Comments, Reviewing, Format Picture/Shape, Thesaurus, Accessibility, Clipboard, Selection) | Navigation (long headings truncate with an ellipsis instead of expanding the pane, #759), Styles, Comments (balloons and cards read in both themes, #2045), Thesaurus, Restrict Editing, Accessibility (grouped findings, jump-to, fixes) and Alt Text (#386), Read Aloud, Zotero; Style Inspector (#236); Clipboard pane (#264) open | all | 70% | 4–6 |
| Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339; Update Field on a table of contents entry updates the table, #1197, and after a save and reopen, #2182); right-click selects a picture, shape or text box and offers Wrap Text (styles, Move with Text / Fix Position on Page, #513); tables, the items following the selection as in Word (Insert, Delete › Cells, Columns, Rows or Table whatever is selected (#1198), Merge and Split Cells, Distribute, AutoFit, Text Direction, Insert Caption…, Table Properties…; no Border Styles yet) | rich per context | 65% | 2–4 |
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268), Options in Word's panes with working settings (#485), Print page with preview, page range and copies (#641); launched with nothing to open, the app starts on Home (the Start screen: Blank document, templates, Recent; no document is open until one is picked or opened, so Esc or Back shows an empty window with the ribbon greyed out but for Help, #1129), with File › Options › General › Start-up to turn it off (#766); Info › Inspect Document finds and removes hidden text by its effective formatting (direct, character or paragraph style) in every story: body, headers, footers, notes, comments and text boxes, and lists what it couldn't reach (#1344); File › Close (Mod+W, Mod+F4) closes the document and keeps the window, showing no page, as Word does; the window's close button quits (#1712) | | 68% | 4–6 |
@@ -106,6 +106,7 @@ Footnote and endnote editing keeps body and note text at normal opacity, on ligh
| 2026-10-15 | trivial | Text Highlight Color: No Color becomes the Highlight button's choice, as in Word, so the next click on the button removes highlighting instead of applying the colour picked before; the ribbon and the mini toolbar share one menu (#2105) |
| 2026-10-16 | trivial | Body and active note text keep normal opacity while editing footnotes or endnotes (#2545); header/footer dimming is unchanged |
| 2026-10-13 | trivial | New/Modify Table Style: No Border on a part that inherits its lines hides them; emptying the font box makes the part's font inherited again (#1288) |
| 2026-10-17 | minor | Insert › Drop Cap is a menu (None, Dropped, In margin, Drop Cap Options…) with a Drop Cap dialog (position, font, lines 1–10, distance 0–22 in), #2601; Modal dialogs 56 → 57 |
| 2026-10-14 | trivial | Tab stops edited on the ruler: corner kind selector, click to set, drag to move or off to clear, per-kind markers mirrored for RTL; each gesture one `para.tabs` call and one undo step; Rulers 85% → 90% |
| 2026-10-12 | trivial | Tables on the page: one selected cell (Select Cell, or a click along its left edge) is a cell selection when its border is dragged: the border moves in its row only (#962) |
| 2026-10-12 | minor | Default keyboard shortcuts expanded to 140 commands on macOS (169 combinations) and 158 on Linux/Windows (188 combinations): Style Inspector (Shift+F1), Macros (Alt+F8), Strikethrough (Mod+Shift+X), Find Next (Shift+F4), Copy/Paste Insert keys (Mod+Insert, Shift+Insert), Go Back (Mod+Alt+Z), File Open/Save/Save As/Print (Mod+Alt+F2, Alt+Shift+F2, Alt+F2, Mod+Alt+I), Field navigation (Alt+F1, Alt+Shift+F1), Outline show first line (Alt+Shift+L), Next Footnote (Alt+Shift+>), Link to Previous (Alt+Shift+R), AutoText (Alt+F3), Spelling on Mac (Mod+Alt+L) |