From da37b7987ea414bdf0ea210871ba45c360c823a0 Mon Sep 17 00:00:00 2001 From: 999sian <71638559+999sian@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:18:03 +0100 Subject: [PATCH] Character border (Home > Font > Character Border) (#60) * Character border (format.border): w:bdr run property, toggle command, boxed rendering * Character border: Home > Font ribbon button and icon * Character border: address review (plain notes), translate ribbon label, fix flaky layout test and clippy nonminimal_bool * Tests: merge the fixture imports after merging main Co-Authored-By: Claude Opus 5.5 * i18n: Spanish and Brazilian Portuguese labels for Character Border Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: WordCraft agent Co-authored-by: 999sian <999sian@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/doc/src/props.rs | 5 ++- crates/doc/src/resolve.rs | 7 +++- crates/docx/src/read/props.rs | 1 + crates/docx/src/write/props.rs | 3 ++ crates/docx/tests/fixtures.rs | 14 +++++++- crates/docx/tests/roundtrip.rs | 15 ++++++++ crates/engine/src/cmd/format.rs | 8 ++++- crates/engine/src/tests.rs | 28 ++++++++++++++- crates/layout/src/display.rs | 54 ++++++++++++++++++++++------- crates/layout/src/hit.rs | 2 +- crates/layout/src/para.rs | 2 +- crates/layout/src/tests.rs | 54 +++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/icons.rs | 4 +++ crates/ui-egui/src/ribbon.rs | 1 + docs/parity.md | 1 - 20 files changed, 184 insertions(+), 20 deletions(-) diff --git a/crates/doc/src/props.rs b/crates/doc/src/props.rs index 08dea06b7..0f7c1f65d 100644 --- a/crates/doc/src/props.rs +++ b/crates/doc/src/props.rs @@ -250,6 +250,9 @@ pub struct CharProps { /// Character shading (background fill). #[serde(skip_serializing_if = "Option::is_none")] pub shading: Option, + /// Character border (`w:bdr`); `style: None` = explicitly no border. + #[serde(skip_serializing_if = "Option::is_none")] + pub border: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vert_align: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -306,7 +309,7 @@ impl CharProps { /// Apply every `Some` field of `patch` on top of `self`. pub fn overlay(&mut self, patch: &CharProps) { overlay_fields!(self, patch; style, font, size, bold, italic, underline, underline_color, strike, double_strike, color, highlight, - shading, vert_align, caps, small_caps, hidden, spacing, scale, position, kern, outline, shadow, emboss, engrave, lang, no_proof, rtl, + shading, border, vert_align, caps, small_caps, hidden, spacing, scale, position, kern, outline, shadow, emboss, engrave, lang, no_proof, rtl, link, ins, del); } pub fn overlaid(mut self, patch: &CharProps) -> CharProps { diff --git a/crates/doc/src/resolve.rs b/crates/doc/src/resolve.rs index 341386947..0a2f364b2 100644 --- a/crates/doc/src/resolve.rs +++ b/crates/doc/src/resolve.rs @@ -3,7 +3,9 @@ use serde::{Deserialize, Serialize}; -use crate::props::{Align, Borders, CharProps, Highlight, LineSpacing, NumRef, ParaProps, Rgb, TabAlign, TabStop, TextColor, Underline, VertAlign}; +use crate::props::{ + Align, Border, Borders, CharProps, Highlight, LineSpacing, NumRef, ParaProps, Rgb, TabAlign, TabStop, TextColor, Underline, VertAlign, +}; use crate::styles::{StyleKind, StyleSheet}; /// Concrete character formatting. @@ -21,6 +23,8 @@ pub struct ResolvedChar { pub color: TextColor, pub highlight: Option, pub shading: Option, + /// Visible character border only (`nil`/`none` resolve to `None`). + pub border: Option, pub vert_align: VertAlign, pub caps: bool, pub small_caps: bool, @@ -53,6 +57,7 @@ impl ResolvedChar { color: c.color.unwrap_or_default(), highlight: c.highlight.and_then(Highlight::rgb), shading: c.shading, + border: c.border.filter(Border::is_visible), vert_align: c.vert_align.unwrap_or_default(), caps: c.caps.unwrap_or(false), small_caps: c.small_caps.unwrap_or(false), diff --git a/crates/docx/src/read/props.rs b/crates/docx/src/read/props.rs index 912a0dc0b..2f5b1d8c7 100644 --- a/crates/docx/src/read/props.rs +++ b/crates/docx/src/read/props.rs @@ -90,6 +90,7 @@ impl PropCtx { } c.underline_color = k.attr("w:color").and_then(Rgb::parse); } + "w:bdr" => c.border = Some(border(k)), "w:shd" => c.shading = shd_fill(k), "w:vertAlign" => { c.vert_align = k.attr("w:val").map(|v| match v { diff --git a/crates/docx/src/write/props.rs b/crates/docx/src/write/props.rs index 93d0fdd5b..26e7f45f7 100644 --- a/crates/docx/src/write/props.rs +++ b/crates/docx/src/write/props.rs @@ -72,6 +72,9 @@ pub fn rpr_inner(w: &mut W, c: &CharProps) { None => w.val("w:u", u.ooxml()), } } + if let Some(b) = &c.border { + border_el(w, "w:bdr", b); + } if let Some(s) = c.shading { w.empty("w:shd", &[("w:val", "clear"), ("w:color", "auto"), ("w:fill", &s.hex())]); } diff --git a/crates/docx/tests/fixtures.rs b/crates/docx/tests/fixtures.rs index 02cfa8865..b51779b3f 100644 --- a/crates/docx/tests/fixtures.rs +++ b/crates/docx/tests/fixtures.rs @@ -3,7 +3,7 @@ use std::io::Write; use wordcraft_doc::para::{Anchor, FloatAlign, NoteKind, ShapeKind, Wrap}; -use wordcraft_doc::props::{Align, TextColor, VMerge}; +use wordcraft_doc::props::{Align, Border, BorderStyle, Rgb, TextColor, VMerge}; use wordcraft_doc::{Block, Document, InlineObject, Paragraph}; const W_NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office""#; @@ -445,6 +445,18 @@ fn header_self_reference_and_escaping_targets() { wordcraft_docx::read(&out).unwrap(); } +#[test] +fn char_border_reads_and_nil_resolves_off() { + let d = read_body( + r#"ab"#, + ); + let p = paras(&d)[0]; + assert_eq!(p.props_of_char(0).border, Some(Border { style: BorderStyle::Single, width: 1.0, color: Some(Rgb(0xFF, 0, 0)), space: 1.0 })); + let nil = p.props_of_char(1); + assert_eq!(nil.border.map(|b| b.style), Some(BorderStyle::None)); + assert_eq!(d.styles.resolve_char(None, nil).border, None); +} + #[test] fn anchor_alignment_reference_areas_and_distances() { let shape = |pos: &str, dist: &str| { diff --git a/crates/docx/tests/roundtrip.rs b/crates/docx/tests/roundtrip.rs index 139cc9d6f..e4d06dd9c 100644 --- a/crates/docx/tests/roundtrip.rs +++ b/crates/docx/tests/roundtrip.rs @@ -66,6 +66,7 @@ fn every_char_prop_round_trips() { color: Some(TextColor::Rgb(Rgb(0xAA, 0x10, 0x20))), highlight: Some(Highlight::Turquoise), shading: Some(Rgb(0xEE, 0xEE, 0x00)), + border: Some(Border { style: BorderStyle::Double, width: 1.5, color: Some(Rgb(0xC0, 0, 0)), space: 2.0 }), vert_align: Some(VertAlign::Superscript), caps: Some(true), small_caps: Some(false), @@ -889,6 +890,20 @@ fn ensure_empty_and_table_end_document_is_valid() { assert_eq!(r.body.len(), 1); } +#[test] +fn char_border_written_between_u_and_shd() { + let c = + CharProps { border: Some(Border::single(0.5)), underline: Some(Underline::Single), shading: Some(Rgb(0xFF, 0xFF, 0)), ..Default::default() }; + let bytes = wordcraft_docx::write(&doc_with(vec![para_runs(&[("x", c)])])).unwrap(); + 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(); + let bdr = r#""#; + assert_eq!(xml.matches(bdr).count(), 1, "{xml}"); + let at = xml.find(bdr).unwrap(); + assert!(xml.find(" Vec { .key("Mod+Shift+W"), CommandSpec::new("format.strikethrough", "Strikethrough", "Home › Font", |s, v| toggle(s, v, |r| r.strike, |c, on| c.strike = Some(on))) .params(r#"{"value"?: bool}"#), + // Note: off clears the direct property only; a border inherited from a character style stays on. + CommandSpec::new("format.border", "Character Border", "Home › Font", |s, v| { + toggle(s, v, |r| r.border.is_some(), |c, on| c.border = on.then(|| Border::single(0.5))) + }) + .params(r#"{"value"?: bool}"#), CommandSpec::new("format.doubleStrikethrough", "Double Strikethrough", "Home › Font › Font", |s, v| { toggle(s, v, |r| r.double_strike, |c, on| c.double_strike = Some(on)) }), @@ -407,6 +412,7 @@ pub fn state(s: &Session) -> Value { "italic": all.iter().all(|r| r.italic), "underline": all.iter().all(|r| r.underline != Underline::None), "strike": all.iter().all(|r| r.strike), + "border": all.iter().all(|r| r.border.is_some()), "subscript": all.iter().all(|r| r.vert_align == VertAlign::Subscript), "superscript": all.iter().all(|r| r.vert_align == VertAlign::Superscript), "color": same(&|r| json!(match r.color { TextColor::Auto => "auto".to_string(), TextColor::Rgb(c) => c.hex() })), diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index d79592be6..e7c8edcc9 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -198,6 +198,32 @@ fn bold_toggles_selection_and_caret_word() { assert_eq!(p.props_of_char(9).italic, None); } +#[test] +fn border_toggles_and_saves() { + use wordcraft_doc::props::Border; + let border = |s: &Session| s.doc.para_at(&Pos::body(0, 0)).unwrap().props_of_char(5).border; + // Saved .docx carries the border iff it is on (read back: the reader maps only `w:bdr` to `border`). + let docx_has_bdr = |s: &Session| { + let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap(); + back.para_at(&Pos::body(0, 0)).unwrap().props_of_char(5).border.is_some() + }; + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "make this boxed"})); + run(&mut s, "select.text", json!({"text": "this"})); + run(&mut s, "format.border", json!({})); + assert_eq!(border(&s), Some(Border::single(0.5))); + assert_eq!(s.doc.para_at(&Pos::body(0, 0)).unwrap().props_of_char(0).border, None); + assert!(docx_has_bdr(&s)); + run(&mut s, "format.border", json!({})); + assert_eq!(border(&s), None); + run(&mut s, "format.border", json!({"value": true})); + run(&mut s, "format.border", json!({"value": true})); + assert_eq!(border(&s), Some(Border::single(0.5))); + run(&mut s, "format.border", json!({"value": false})); + assert_eq!(border(&s), None); + assert!(!docx_has_bdr(&s)); +} + #[test] fn pending_format_applies_to_typing() { let mut s = s(); @@ -248,7 +274,7 @@ fn applying_a_heading_clears_list_numbering() { ); run(&mut s, style_cmd, json!({})); assert!( - !s.doc.para_at(&Pos::body(0, 0)).unwrap().props.numbering.is_some_and(|n| n.num != 0), + s.doc.para_at(&Pos::body(0, 0)).unwrap().props.numbering.is_none_or(|n| n.num == 0), "{style_cmd}: should clear direct list numbering" ); } diff --git a/crates/layout/src/display.rs b/crates/layout/src/display.rs index 0ef191339..c00b01388 100644 --- a/crates/layout/src/display.rs +++ b/crates/layout/src/display.rs @@ -1,7 +1,7 @@ //! Page → draw items, shared by the raster renderer, the PDF exporter and thumbnails. use wordcraft_doc::para::{InlineObject, ShapeKind}; -use wordcraft_doc::props::{BorderStyle, Rgb, TextColor, Underline}; +use wordcraft_doc::props::{Border, BorderStyle, Rgb, TextColor, Underline}; use wordcraft_doc::{Document, Path, StoryRef}; use wordcraft_fonts::FaceRef; use wordcraft_geom::Rect; @@ -131,19 +131,34 @@ fn border_stroke(s: BorderStyle) -> Stroke { } } +/// A border line from `(x0, y0)` to `(x1, y1)` (paragraph rules and character borders). +fn rule(x0: f32, y0: f32, x1: f32, y1: f32, b: &Border, alpha: f32) -> Draw { + Draw::Line { + x0, + y0, + x1, + y1, + width: if b.style == BorderStyle::Thick { b.width.max(1.5) } else { b.width.max(0.25) }, + color: b.color.unwrap_or(Rgb::BLACK), + stroke: border_stroke(b.style), + alpha, + } +} + +/// Closed box around one line segment of a character-border group, widened by `space`. +// Note: border drawn outside text advance, reserve width in line breaking if overlap shows. +fn char_box(b: &Border, x0: f32, x1: f32, top: f32, bottom: f32, alpha: f32, out: &mut Vec) { + let (l, r) = (x0 - b.space, x1 + b.space); + out.push(rule(l, top, r, top, b, alpha)); + out.push(rule(l, bottom, r, bottom, b, alpha)); + out.push(rule(l, top, l, bottom, b, alpha)); + out.push(rule(r, top, r, bottom, b, alpha)); +} + fn item(doc: &Document, it: &Placed, opts: &DisplayOptions, alpha: f32, out: &mut Vec) { match it { Placed::Fill { rect, color } => out.push(Draw::Fill { rect: *rect, color: *color, alpha }), - Placed::Rule { x0, y0, x1, y1, border } => out.push(Draw::Line { - x0: *x0, - y0: *y0, - x1: *x1, - y1: *y1, - width: if border.style == BorderStyle::Thick { border.width.max(1.5) } else { border.width.max(0.25) }, - color: border.color.unwrap_or(Rgb::BLACK), - stroke: border_stroke(border.style), - alpha, - }), + Placed::Rule { x0, y0, x1, y1, border } => out.push(rule(*x0, *y0, *x1, *y1, border, alpha)), Placed::Image { rect, media, crop, .. } => out.push(Draw::Image { rect: *rect, media: media.clone(), crop: *crop, alpha }), Placed::Shape { rect, kind, fill, stroke, stroke_width } => { out.push(Draw::Shape { rect: *rect, kind: *kind, fill: *fill, stroke: *stroke, stroke_width: *stroke_width }) @@ -202,7 +217,8 @@ fn lines( } } } - // Backgrounds first: highlight and character shading. + // Backgrounds first: highlight and character shading; character borders group equal adjacent borders. + let mut open: Option<(Border, f32, f32)> = None; for k in line.c0..line.c1 { let (Some(c), Some(cx), Some(nx)) = (pl.clusters.get(k), line.xs.get(k - line.c0), line.xs.get(k + 1 - line.c0)) else { continue }; let Some(st) = pl.styles.get(c.style as usize) else { continue }; @@ -212,6 +228,20 @@ fn lines( if let Some(h) = st.rc.highlight.or(st.rc.shading) { out.push(Draw::Fill { rect: Rect::new(x + cx, top, (nx - cx).max(0.0), line.height), color: h, alpha }); } + let b = st.rc.border; + if let Some((ob, _, x1)) = open.as_mut() + && Some(*ob) == b + { + *x1 = x + nx; + continue; + } + if let Some((ob, x0, x1)) = open.take() { + char_box(&ob, x0, x1, top, bottom, alpha, out); + } + open = b.map(|b| (b, x + cx, x + nx)); + } + if let Some((ob, x0, x1)) = open { + char_box(&ob, x0, x1, top, bottom, alpha, out); } // Label. if li == 0 diff --git a/crates/layout/src/hit.rs b/crates/layout/src/hit.rs index 6d4cd8f21..3cc79f9a0 100644 --- a/crates/layout/src/hit.rs +++ b/crates/layout/src/hit.rs @@ -228,7 +228,7 @@ impl DocLayout { let Some(l) = para.lines.get(li) else { continue }; let from = if path == a.path { a.off.max(l.start) } else { l.start }; let to = if path == b.path { b.off.min(l.stop) } else { l.stop }; - if path == a.path && a.off > l.stop && !(l.stop == a.off) { + if path == a.path && a.off > l.stop { continue; } if path == b.path && b.off < l.start { diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 693ca9f33..6eb6d4795 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -194,7 +194,7 @@ impl<'a> Builder<'a> { face.id(), rc.strike || rc.double_strike || rc.link.is_some(), ); - let key = (format!("{}|{:?}|{:?}|{:?}|{:?}|{}", key.0, rc.highlight, rc.shading, rc.ins, rc.del, rc.hidden), key.1, key.2); + let key = (format!("{}|{:?}|{:?}|{:?}|{:?}|{}|{:?}", key.0, rc.highlight, rc.shading, rc.ins, rc.del, rc.hidden, rc.border), key.1, key.2); if let Some(i) = self.style_index.get(&key) { return *i; } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index f6c3d103b..9782ee247 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -275,6 +275,60 @@ fn display_has_glyphs_and_marks() { assert!(items.iter().any(|i| matches!(i, display::Draw::Mark { ch: '→', .. }))); } +fn border_lines(d: &Document) -> Vec<(f32, f32, f32, f32)> { + border_lines_in(d, &lay(d)) +} + +fn border_lines_in(d: &Document, l: &DocLayout) -> Vec<(f32, f32, f32, f32)> { + display::page_display(d, &l.pages[0], &display::DisplayOptions::default()) + .into_iter() + .filter_map(|i| if let display::Draw::Line { x0, y0, x1, y1, .. } = i { Some((x0, y0, x1, y1)) } else { None }) + .collect() +} + +#[test] +fn character_border_draws_one_box_around_run() { + use wordcraft_doc::props::Border; + let mut d = Document::from_text("Hello world"); + assert!(border_lines(&d).is_empty()); + d.format_range(&Pos::body(0, 0), &Pos::body(0, 5), &|c| c.border = Some(Border::single(0.5))).unwrap(); + // One layout for both the draw list and the expected geometry: the background font scan can change metrics between layouts. + let l = lay(&d); + let lines = border_lines_in(&d, &l); + assert_eq!(lines.len(), 4, "{lines:?}"); + let Some(Placed::Lines { para, x, y, .. }) = l.pages[0].items.iter().find(|i| matches!(i, Placed::Lines { .. })) else { panic!() }; + let (gx0, gx1) = (x + para.x_of(0, 0).unwrap(), x + para.x_of(0, 5).unwrap()); + let (top, bottom) = (*y, y + para.lines[0].height); + let horiz: Vec<_> = lines.iter().filter(|l| l.1 == l.3).collect(); + let vert: Vec<_> = lines.iter().filter(|l| l.0 == l.2).collect(); + assert_eq!(horiz.len(), 2); + assert_eq!(vert.len(), 2); + assert!(horiz.iter().any(|h| (h.1 - top).abs() < 0.01) && horiz.iter().any(|h| (h.1 - bottom).abs() < 0.01), "{lines:?}"); + assert!(horiz.iter().all(|h| (h.0 - gx0).abs() < 0.01 && (h.2 - gx1).abs() < 0.01), "{lines:?} {gx0} {gx1}"); +} + +#[test] +fn equal_adjacent_borders_share_a_box() { + use wordcraft_doc::props::Border; + let mut d = Document::from_text("ab"); + d.format_range(&Pos::body(0, 0), &Pos::body(0, 2), &|c| c.border = Some(Border::single(0.5))).unwrap(); + d.format_range(&Pos::body(0, 1), &Pos::body(0, 2), &|c| c.bold = Some(true)).unwrap(); + assert_eq!(border_lines(&d).len(), 4); + d.format_range(&Pos::body(0, 1), &Pos::body(0, 2), &|c| c.border = Some(Border::single(1.0))).unwrap(); + assert_eq!(border_lines(&d).len(), 8); +} + +#[test] +fn wrapped_border_boxes_each_line() { + use wordcraft_doc::props::Border; + let text = "boxed ".repeat(20); + let mut d = Document::from_text(text.trim_end()); + let n = d.para_at(&Pos::body(0, 0)).unwrap().len(); + d.format_range(&Pos::body(0, 0), &Pos::body(0, n), &|c| c.border = Some(Border::single(0.5))).unwrap(); + assert_eq!(lines_of(&lay(&d)), 2); + assert_eq!(border_lines(&d).len(), 8); +} + #[test] fn web_view_is_one_page() { let d = Document::from_text(&"text ".repeat(3000)); diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index bf2a3e70f..826a2ed11 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -793,3 +793,4 @@ Dark page (white text on black) Página oscura (texto blanco sobre negro) Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. Muestra los documentos con los colores invertidos, como en Vista › Cambiar modos. Guardar, imprimir y los PDF no cambian. Only web (http, https) and email (mailto) links can be followed Solo se pueden seguir vínculos web (http, https) y de correo electrónico (mailto) + Character Border Borde de carácter diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 1fcdd33eb..0c1a1de35 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -99,6 +99,7 @@ Character Scale 文字の拡大/縮小 Character Shading 文字の網かけ Character Spacing 文字間隔 + Character Border 文字の罫線 Characters (no spaces) 文字数(スペースを含めない) Characters (with spaces) 文字数(スペースを含める) Chart グラフ diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index ba3f6db55..86bb0ae01 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -788,3 +788,4 @@ Apply Styles Aplicar estilos Scroll ribbon left Rolar a faixa de opções para a esquerda Scroll ribbon right Rolar a faixa de opções para a direita + Character Border Borda de caractere diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 25bbd896d..12bef119c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -110,6 +110,7 @@ Character Scale 字符缩放 Character Shading 字符底纹 Character Spacing 字符间距 + Character Border 字符边框 Characters (no spaces) 字符数(不计空格) Characters (with spaces) 字符数(计空格) Chart 图表 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 01617672e..d5477a91e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -100,6 +100,7 @@ Character Scale 字元縮放 Character Shading 字元網底 Character Spacing 字元間距 + Character Border 字元邊框 Characters (no spaces) 字元數(不含空格) Characters (with spaces) 字元數(含空格) Chart 圖表 diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 208298768..cb3928aee 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -116,6 +116,10 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32, accent: Color32) { pen.text(10.0, 10.0, 12.0, "ab", c, false); pen.line_c(&[(3.0, 10.5), (17.0, 10.5)], c); } + "charborder" => { + pen.rect(3.0, 3.0, 17.0, 17.0, a); + pen.text(10.0, 6.0, 9.0, "A", c, false); + } "subscript" => { pen.text(8.0, 9.0, 13.0, "x", c, false); pen.text(15.0, 14.5, 8.0, "2", a, true); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index a74246555..455c848a4 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -273,6 +273,7 @@ fn home(app: &mut WordApp, ui: &mut Ui) { small(ui, app, "strike", None, "Strikethrough", "format.strikethrough", json!({}), flag("strike")); small(ui, app, "subscript", None, "Subscript", "format.subscript", json!({}), flag("subscript")); small(ui, app, "superscript", None, "Superscript", "format.superscript", json!({}), flag("superscript")); + small(ui, app, "charborder", None, "Character Border", "format.border", json!({}), flag("border")); ui.add_space(4.0); menu_button(ui, app, "effects", None, "Text Effects and Typography", false, |ui, app| { mi(ui, app, "Outline", "format.outline", json!({})); diff --git a/docs/parity.md b/docs/parity.md index 2afaa9496..9cf28945f 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -31,7 +31,6 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word- - Clipboard › Paste Special (`edit.pasteSpecial`) - Clipboard › Clipboard Pane (`edit.clipboardPane`) -- Font › Character Border (`format.border`) - Font › Phonetic Guide (`format.phonetic`) - Font › Enclose Characters (`format.enclose`) - Paragraph › Asian Typography (`para.asianTypography`)