mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 11:27:10 -06:00
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 <noreply@anthropic.com> * i18n: Spanish and Brazilian Portuguese labels for Character Border Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: WordCraft agent <agent@localhost> Co-authored-by: 999sian <999sian@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
WordCraft agent
999sian
Brandon Thomas
parent
6e73fe40e3
commit
da37b7987e
@@ -250,6 +250,9 @@ pub struct CharProps {
|
||||
/// Character shading (background fill).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub shading: Option<Rgb>,
|
||||
/// Character border (`w:bdr`); `style: None` = explicitly no border.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border: Option<Border>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vert_align: Option<VertAlign>,
|
||||
#[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 {
|
||||
|
||||
@@ -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<Rgb>,
|
||||
pub shading: Option<Rgb>,
|
||||
/// Visible character border only (`nil`/`none` resolve to `None`).
|
||||
pub border: Option<Border>,
|
||||
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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())]);
|
||||
}
|
||||
|
||||
@@ -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#"<w:p><w:r><w:rPr><w:bdr w:val="single" w:sz="8" w:space="1" w:color="FF0000"/></w:rPr><w:t>a</w:t></w:r><w:r><w:rPr><w:bdr w:val="nil"/></w:rPr><w:t>b</w:t></w:r></w:p>"#,
|
||||
);
|
||||
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| {
|
||||
|
||||
@@ -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#"<w:bdr w:val="single" w:sz="4" w:space="0" w:color="auto"/>"#;
|
||||
assert_eq!(xml.matches(bdr).count(), 1, "{xml}");
|
||||
let at = xml.find(bdr).unwrap();
|
||||
assert!(xml.find("<w:u ").unwrap() < at && at < xml.find("<w:shd ").unwrap(), "{xml}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_mode_round_trips() {
|
||||
let mut d = Document::new();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use wordcraft_doc::Pos;
|
||||
use wordcraft_doc::props::{CharProps, Highlight, Rgb, TextColor, Underline, VertAlign};
|
||||
use wordcraft_doc::props::{Border, CharProps, Highlight, Rgb, TextColor, Underline, VertAlign};
|
||||
use wordcraft_doc::resolve::ResolvedChar;
|
||||
|
||||
use super::sel_result;
|
||||
@@ -32,6 +32,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
.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() })),
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Draw>) {
|
||||
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<Draw>) {
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -99,6 +99,7 @@
|
||||
Character Scale 文字の拡大/縮小
|
||||
Character Shading 文字の網かけ
|
||||
Character Spacing 文字間隔
|
||||
Character Border 文字の罫線
|
||||
Characters (no spaces) 文字数(スペースを含めない)
|
||||
Characters (with spaces) 文字数(スペースを含める)
|
||||
Chart グラフ
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -110,6 +110,7 @@
|
||||
Character Scale 字符缩放
|
||||
Character Shading 字符底纹
|
||||
Character Spacing 字符间距
|
||||
Character Border 字符边框
|
||||
Characters (no spaces) 字符数(不计空格)
|
||||
Characters (with spaces) 字符数(计空格)
|
||||
Chart 图表
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -100,6 +100,7 @@
|
||||
Character Scale 字元縮放
|
||||
Character Shading 字元網底
|
||||
Character Spacing 字元間距
|
||||
Character Border 字元邊框
|
||||
Characters (no spaces) 字元數(不含空格)
|
||||
Characters (with spaces) 字元數(含空格)
|
||||
Chart 圖表
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
@@ -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!({}));
|
||||
|
||||
@@ -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`)
|
||||
|
||||
Reference in New Issue
Block a user