M7: Justify, underline, line spacing, character spacing and horizontal scale for existing paragraphs (and justify for added text); Lucide align-justify icon

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 03:16:03 -04:00
1 parent 385313e0bf
commit d7c22d7ef0
15 files changed
+241 -32

No files matched your search

+2 -1
View File
@@ -4,13 +4,14 @@
Every asset PrintCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
## In this repository (238)
## In this repository (239)
| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-justify.svg` | Lucide icon "align-justify" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-justify.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-right.svg` | Lucide icon "align-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/columns-3.svg` | Lucide icon "columns-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/columns-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rows-3.svg` | Lucide icon "rows-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rows-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
+11
View File
@@ -41,6 +41,17 @@ kind = "icon"
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
sha256 = "bc70fba645343be0760d1a87f9e0975a10ead2b8d95883314f4cea007376e79f"
[[asset]]
path = "assets/icons/align-justify.svg"
title = "Lucide icon \"align-justify\""
author = "Lucide Contributors (portions Cole Bemis, Feather)"
source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-justify.svg)"
licence = "ISC"
licence_file = "assets/icons/LICENSE-lucide.txt"
kind = "icon"
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
sha256 = "df46328a73b53ae85f6837d170411b2616204c6cedc59bd8603112b5b38b3b3e"
[[asset]]
path = "assets/icons/align-right.svg"
title = "Lucide icon \"align-right\""
+17
View File
@@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-align-justify"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 5h18" />
<path d="M3 12h18" />
<path d="M3 19h18" />
</svg>

After

Width:  |  Height:  |  Size: 363 B

+1
View File
@@ -57,6 +57,7 @@ fn style(a: &Args, t: &mut AddedText) -> Result<()> {
"left" => TextAlign::Left,
"center" | "centre" => TextAlign::Center,
"right" => TextAlign::Right,
"justify" => TextAlign::Justify,
other => return Err(bad(format!("unknown align {other:?}"))),
};
}
+9 -1
View File
@@ -390,7 +390,14 @@ impl Automation {
}
let block = self.doc(&a)?.text_blocks(page)[k as usize - 1].clone();
let text = a.opt_str("text")?.map(str::to_owned).unwrap_or(block.text);
let mut style = printcraft_engine::BlockStyle { size: a.opt_num("size")?, ..Default::default() };
let mut style = printcraft_engine::BlockStyle {
size: a.opt_num("size")?,
underline: a.opt_bool("underline")?,
line_spacing: a.opt_num("line_spacing")?,
char_spacing: a.opt_num("char_spacing")?,
scale: a.opt_num("scale")?,
..Default::default()
};
if let Some(f) = a.opt_str("font")? {
let family = match f {
"helvetica" => printcraft_engine::FontFamily::Helvetica,
@@ -408,6 +415,7 @@ impl Automation {
"left" => printcraft_engine::TextAlign::Left,
"center" => printcraft_engine::TextAlign::Center,
"right" => printcraft_engine::TextAlign::Right,
"justify" => printcraft_engine::TextAlign::Justify,
other => return Err(ToolError::InvalidArgs(format!("unknown align {other:?}"))),
});
}
+8 -4
View File
@@ -410,7 +410,7 @@ pub fn tools() -> Vec<ToolDef> {
"rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 },
"font": { "type": "string", "enum": ["helvetica", "times", "courier"] }, "size": { "type": "number", "minimum": 1, "maximum": 500 },
"bold": { "type": "boolean" }, "italic": { "type": "boolean" }, "color": { "type": "string" },
"align": { "type": "string", "enum": ["left", "center", "right"] },
"align": { "type": "string", "enum": ["left", "center", "right", "justify"] },
}),
&["doc", "page", "text"],
)),
@@ -431,7 +431,7 @@ pub fn tools() -> Vec<ToolDef> {
"rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 }, "text": { "type": "string" },
"font": { "type": "string", "enum": ["helvetica", "times", "courier"] }, "size": { "type": "number", "minimum": 1, "maximum": 500 },
"bold": { "type": "boolean" }, "italic": { "type": "boolean" }, "color": { "type": "string" },
"align": { "type": "string", "enum": ["left", "center", "right"] },
"align": { "type": "string", "enum": ["left", "center", "right", "justify"] },
"rotate": { "type": "integer" }, "flip_h": { "type": "boolean" }, "flip_v": { "type": "boolean" },
"crop": { "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 0.49 }, "minItems": 4, "maxItems": 4 },
"image": { "type": "string", "description": "Replace the picture with this file." },
@@ -842,7 +842,7 @@ pub fn tools() -> Vec<ToolDef> {
.ro()
.cmd("edit.edit_text")
.with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])),
t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb) and align (left, center, right); text may then be omitted. Its own font is reused when it can show every character; otherwise the line is set in Helvetica (the result shows the font used). Text that no available font can show is refused. Undoable.")
t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent); text may then be omitted. Its own font is reused when it can show every character; otherwise the line is set in Helvetica (the result shows the font used). Text that no available font can show is refused. Undoable.")
.cmd("edit.edit_text")
.with(schema(
json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "line": { "type": "integer", "minimum": 1 },
@@ -853,7 +853,11 @@ pub fn tools() -> Vec<ToolDef> {
"italic": { "type": "boolean" },
"size": { "type": "number", "exclusiveMinimum": 0 },
"color": { "type": "string", "description": "#rrggbb" },
"align": { "type": "string", "enum": ["left", "center", "right"] } }),
"align": { "type": "string", "enum": ["left", "center", "right", "justify"] },
"underline": { "type": "boolean" },
"line_spacing": { "type": "number", "description": "Multiple of the font size (1.2 is ordinary)." },
"char_spacing": { "type": "number", "description": "Points." },
"scale": { "type": "number", "description": "Horizontal scale in percent." } }),
&["doc", "page"],
)),
t("doc_revisions", "List revisions", "List the document's saved revisions (oldest first): each incremental update is one. Returns revision number, where it ends in the file and its size, and which signatures sign exactly that revision.")
+11 -2
View File
@@ -79,6 +79,8 @@ pub enum Align {
Left,
Center,
Right,
/// Lines (but the last) stretched to the box's width.
Justify,
}
#[derive(Clone, Debug, PartialEq)]
@@ -246,13 +248,18 @@ fn draw(doc: &Document, c: &Content, view: [f64; 6]) -> Result<(Vec<u8>, Dict),
for (i, line) in lines(t).iter().enumerate() {
let w = t.family.width(line, t.size, t.bold);
let x = match t.align {
Align::Left => r[0],
Align::Left | Align::Justify => r[0],
Align::Center => r[0] + ((r[2] - r[0]) - w) / 2.0,
Align::Right => r[2] - w,
};
// Baseline: 0.8 em below the line top.
let y = r[3] - (i as f64 * 1.2 + 0.95) * t.size;
out.extend(format!("1 0 0 1 {} {} Tm ", n(x), n(y)).bytes());
// Justified: word spacing makes every line but the last fill the box.
let all = lines(t);
let spaces = line.matches(' ').count();
let tw =
if t.align == Align::Justify && i + 1 < all.len() && spaces > 0 { ((r[2] - r[0]) - w).max(0.0) / spaces as f64 } else { 0.0 };
out.extend(format!("1 0 0 1 {} {} Tm {} Tw ", n(x), n(y), n(tw)).bytes());
out.extend(literal(&win_ansi(line)));
out.extend_from_slice(b" Tj\n");
}
@@ -307,6 +314,7 @@ fn params(c: &Content) -> Dict {
Align::Left => 0,
Align::Center => 1,
Align::Right => 2,
Align::Justify => 3,
}),
);
d.set(b"Rect".to_vec(), arr(&text_rect(t)));
@@ -350,6 +358,7 @@ fn parse(doc: &Document, d: &Dict) -> Option<Content> {
align: match d.int(b"Align") {
Some(1) => Align::Center,
Some(2) => Align::Right,
Some(3) => Align::Justify,
_ => Align::Left,
},
}))
+32
View File
@@ -424,6 +424,7 @@ fn paragraphs_take_new_formatting() {
size: Some(12.0),
color: Some([1.0, 0.0, 0.0]),
align: Some(added::Align::Center),
..Default::default()
};
text::rewrite_block(&mut doc, 0, 0, None, &style).unwrap();
let doc = reopen(&doc);
@@ -447,3 +448,34 @@ fn paragraphs_take_new_formatting() {
assert!((l.rect[2] - right).abs() < 1.0, "{:?} ends at {} not {right}", l.text, l.rect[2]);
}
}
#[test]
fn justify_underline_and_spacing() {
let src = "BT /F1 10 Tf 12 TL 100 700 Td (One two three four five six seven) Tj T* (eight nine ten eleven twelve) Tj T* (end) Tj ET";
// Justified: every line but the last reaches the right edge.
let mut doc = text_page(src);
let right = text::text_blocks(&doc, 0).unwrap()[0].rect[2];
text::rewrite_block(&mut doc, 0, 0, None, &text::BlockStyle { align: Some(added::Align::Justify), ..Default::default() }).unwrap();
let lines = text::text_lines(&doc, 0).unwrap();
for l in &lines[..lines.len() - 1] {
assert!((l.rect[2] - right).abs() < 1.0, "{:?} ends at {} not {right}", l.text, l.rect[2]);
}
// Double line spacing, underlined.
let mut doc = text_page(src);
let style = text::BlockStyle { line_spacing: Some(2.0), underline: Some(true), ..Default::default() };
text::rewrite_block(&mut doc, 0, 0, None, &style).unwrap();
let lines = text::text_lines(&doc, 0).unwrap();
assert!((lines[0].origin_baseline() - lines[1].origin_baseline() - 20.0).abs() < 0.01);
let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned();
assert!(content.contains(" l\nS\n") || content.contains(" l S"), "{content}");
// Character spacing widens, horizontal scale narrows.
let mut doc = text_page("BT /F1 10 Tf 100 700 Td (Wide text) Tj ET");
let w0 = text::text_lines(&doc, 0).unwrap()[0].rect;
text::rewrite_block(&mut doc, 0, 0, None, &text::BlockStyle { char_spacing: Some(2.0), ..Default::default() }).unwrap();
let w1 = text::text_lines(&doc, 0).unwrap()[0].rect;
assert!((w1[2] - w1[0]) - (w0[2] - w0[0]) > 15.0, "{w0:?} → {w1:?}");
let mut doc = text_page("BT /F1 10 Tf 100 700 Td (Wide text) Tj ET");
text::rewrite_block(&mut doc, 0, 0, None, &text::BlockStyle { scale: Some(50.0), ..Default::default() }).unwrap();
let w2 = text::text_lines(&doc, 0).unwrap()[0].rect;
assert!(((w2[2] - w2[0]) - (w0[2] - w0[0]) / 2.0).abs() < 1.0, "{w0:?} → {w2:?}");
}
+65 -5
View File
@@ -566,6 +566,13 @@ pub struct BlockStyle {
/// Fill colour (RGB 0–1).
pub color: Option<[f64; 3]>,
pub align: Option<crate::added::Align>,
/// Draw a line under each line of text.
pub underline: Option<bool>,
/// Line spacing as a multiple of the font size (1.2 is ordinary).
pub line_spacing: Option<f64>,
/// Character spacing (points) and horizontal scaling (percent).
pub char_spacing: Option<f64>,
pub scale: Option<f64>,
}
/// Rewrite paragraph `block` with new text (or its own) and formatting, rewrapped to its width.
@@ -587,6 +594,15 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
let k = o.k.max(1e-6);
// The size in text space: points ÷ the text-to-user scale.
let size = style.size.map_or(old_size, |pt| (pt / k).max(0.1));
// Spacing and scaling (text state), with the chosen values.
let mut ts_state = o.state.clone();
if let Some(c) = style.char_spacing {
ts_state.char_spacing = c / k;
}
if let Some(sc) = style.scale {
ts_state.scale = (sc / 100.0).clamp(0.1, 10.0);
}
let o_state = ts_state.clone();
let metrics = fonts_res.get(font_name.as_bytes()).and_then(|f| doc.resolve(f).as_dict().cloned()).map(|d| Metrics::from_dict(doc, &d));
let reuse = style.family.is_none() && metrics.as_ref().is_some_and(|m| m.encode(&text).is_some());
// The standard font used when the paragraph's own can't be (chosen, or substituted).
@@ -611,13 +627,13 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
.map(|bytes| {
m.codes(&bytes)
.iter()
.map(|(c, l)| m.width(*c) * size + o.state.char_spacing + if m.is_space(*c, *l) { o.state.word_spacing } else { 0.0 })
.map(|(c, l)| m.width(*c) * size + o_state.char_spacing + if m.is_space(*c, *l) { o_state.word_spacing } else { 0.0 })
.sum::<f64>()
})
.unwrap_or(0.0),
_ => std_width(s, size),
_ => std_width(s, size) + s.chars().count() as f64 * o_state.char_spacing,
};
t * o.state.scale * k
t * o_state.scale * k
};
let wrapped = wrap(&text, width + 0.5, advance);
let mut substituted = None;
@@ -645,10 +661,15 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
(name, Box::new(|s: &str| Some(printcraft_fonts::win_ansi(s))))
};
// Line spacing in text space: the paragraph's own (scaled with the size), or 1.2 × the size.
let lead = if members.len() > 1 { (members[0].origin.baseline - members[1].origin.baseline) / k * size / old_size } else { size * 1.2 };
let lead = match style.line_spacing {
Some(m) => size * m.clamp(0.5, 5.0),
None if members.len() > 1 => (members[0].origin.baseline - members[1].origin.baseline) / k * size / old_size,
None => size * 1.2,
};
let n = printcraft_content::num;
let mut block_ops = vec![Op::new("BT", vec![])];
let mut state = o.state.clone();
let mut state = o_state.clone();
state.word_spacing = 0.0;
state.font = Some((show_font, size));
if let Some([r, g, bl]) = style.color {
state.fill = vec![Op::new("rg", vec![n(r), n(g), n(bl)])];
@@ -664,17 +685,56 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
_ => 0.0,
}
};
// Justify: word spacing (single-byte code 32 only) so each line but the last fills the width.
let single_byte = !reuse || metrics.as_ref().is_some_and(|m| !m.composite);
let justify = style.align == Some(crate::added::Align::Justify) && single_byte;
let mut x = 0.0;
let mut tw_set = 0.0;
let mut underlines: Vec<(f64, f64, f64)> = Vec::new(); // (x0, x1, y) in text space
for (i, line) in wrapped.iter().enumerate() {
let dx = offset(line);
if i > 0 || dx != 0.0 {
block_ops.push(Op::new("Td", vec![n(dx - x), n(if i > 0 { -lead } else { 0.0 })]));
}
x = dx;
let spaces = line.matches(' ').count();
let tw =
if justify && i + 1 < wrapped.len() && spaces > 0 { (width - advance(line)).max(0.0) / k / o_state.scale / spaces as f64 } else { 0.0 };
if tw != tw_set {
block_ops.push(Op::new("Tw", vec![n(tw)]));
tw_set = tw;
}
let bytes = encode(line).ok_or_else(|| EditError::Invalid(format!("\"{line}\" can't be shown")))?;
block_ops.push(Op::new("Tj", vec![Object::String(PdfString::literal(bytes))]));
let w = (advance(line) + tw * spaces as f64 * o_state.scale * k) / k;
underlines.push((dx, dx + w, -(i as f64) * lead - size * 0.12));
}
block_ops.push(Op::new("ET", vec![]));
if style.underline == Some(true) {
// In text space (the paragraph's text matrix), in its fill colour.
let colour: Vec<Op> = state
.fill
.iter()
.map(|f| match f.op.as_slice() {
b"g" => Op::new("G", f.operands.clone()),
b"rg" => Op::new("RG", f.operands.clone()),
b"k" => Op::new("K", f.operands.clone()),
b"cs" => Op::new("CS", f.operands.clone()),
b"sc" => Op::new("SC", f.operands.clone()),
_ => Op::new("SCN", f.operands.clone()),
})
.collect();
block_ops.push(Op::new("q", vec![]));
block_ops.push(Op::new("cm", o.tm.iter().map(|v| n(*v)).collect()));
block_ops.extend(colour);
block_ops.push(Op::new("w", vec![n(size * 0.06)]));
for (x0, x1, y) in underlines {
block_ops.push(Op::new("m", vec![n(x0), n(y)]));
block_ops.push(Op::new("l", vec![n(x1), n(y)]));
}
block_ops.push(Op::new("S", vec![]));
block_ops.push(Op::new("Q", vec![]));
}
// What the text after the paragraph expects: the state at its BT.
block_ops.extend(o.bt_state.ops().into_iter().filter(|op| !op.is("Tf") || o.bt_state.font.is_some()));
let drop: std::collections::HashSet<usize> = members.iter().flat_map(|l| l.ops.iter().copied()).collect();
+1
View File
@@ -349,6 +349,7 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) ->
(TextAlign::Left, "align-left", "Align left"),
(TextAlign::Center, "align-center", "Centre"),
(TextAlign::Right, "align-right", "Align right"),
(TextAlign::Justify, "align-justify", "Justify"),
] {
if crate::icons::button(ui, icon, 26.0, s.align == a, tip).clicked() {
s.align = a;
+52
View File
@@ -24,6 +24,50 @@ pub struct LineEditor {
/// The Format text panel's values, and what the paragraph had (to send only changes).
pub look: printcraft_engine::AddedText,
look0: printcraft_engine::AddedText,
/// Underline, line spacing (× size; 0 = the paragraph's own), character spacing (pt) and
/// horizontal scale (%), and what they were.
pub extras: Extras,
extras0: Extras,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Extras {
pub underline: bool,
pub line_spacing: f64,
pub char_spacing: f64,
pub scale: f64,
}
impl Default for Extras {
fn default() -> Self {
Extras { underline: false, line_spacing: 0.0, char_spacing: 0.0, scale: 100.0 }
}
}
/// Underline, line spacing, character spacing and horizontal scale for the paragraph being
/// edited (under Format text). Returns `true` when something changed.
pub(crate) fn extras_panel(ui: &mut egui::Ui, e: &mut Extras) -> bool {
let before = *e;
ui.horizontal(|ui| {
if crate::icons::button(ui, "underline", 26.0, e.underline, "Underline").clicked() {
e.underline = !e.underline;
}
let label = |v: f64| if v == 0.0 { "Line spacing".to_string() } else { format!("{v:.2}×") };
egui::ComboBox::from_id_salt("line-spacing").selected_text(label(e.line_spacing)).width(110.0).show_ui(ui, |ui| {
for v in [1.0, 1.15, 1.5, 2.0] {
ui.selectable_value(&mut e.line_spacing, v, label(v));
}
});
});
ui.horizontal(|ui| {
let l = ui.label("Character spacing");
ui.add(egui::DragValue::new(&mut e.char_spacing).range(-5.0..=50.0).speed(0.1).suffix(" pt")).labelled_by(l.id);
});
ui.horizontal(|ui| {
let l = ui.label("Horizontal scale");
ui.add(egui::DragValue::new(&mut e.scale).range(10.0..=400.0).speed(1.0).suffix(" %")).labelled_by(l.id);
});
*e != before
}
impl LineEditor {
@@ -35,12 +79,18 @@ impl LineEditor {
size: (l.size != o.size).then_some(l.size),
color: (l.color != o.color).then_some(l.color),
align: (l.align != o.align).then_some(l.align),
underline: (self.extras.underline != self.extras0.underline).then_some(self.extras.underline),
line_spacing: (self.extras.line_spacing != self.extras0.line_spacing && self.extras.line_spacing > 0.0)
.then_some(self.extras.line_spacing),
char_spacing: (self.extras.char_spacing != self.extras0.char_spacing).then_some(self.extras.char_spacing),
scale: (self.extras.scale != self.extras0.scale).then_some(self.extras.scale),
}
}
/// After applying formatting: the paragraph now has it.
pub fn applied(&mut self) {
self.look0 = self.look.clone();
self.extras0 = self.extras;
self.original = self.text.clone();
self.focus = true;
}
@@ -243,6 +293,8 @@ pub(crate) fn page_input(
focus: true,
look: look_of(l),
look0: look_of(l),
extras: Extras::default(),
extras0: Extras::default(),
});
}
true
+1
View File
@@ -4,6 +4,7 @@
pub static ICONS: &[(&str, &[u8])] = &[
("accessibility", include_bytes!("../../../assets/icons/accessibility.svg")),
("align-center", include_bytes!("../../../assets/icons/align-center.svg")),
("align-justify", include_bytes!("../../../assets/icons/align-justify.svg")),
("align-left", include_bytes!("../../../assets/icons/align-left.svg")),
("align-right", include_bytes!("../../../assets/icons/align-right.svg")),
("app-window", include_bytes!("../../../assets/icons/app-window.svg")),
+6 -1
View File
@@ -277,9 +277,14 @@ fn format_section(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) {
if let Some((i, _)) = app.active_ids()
&& let Some(ed) = app.views[i].line_editor.clone()
{
let mut ed = ed;
let mut changed = false;
if let Some(look) = crate::content_ui::format_panel(ui, t, &ed.look) {
let mut ed = ed;
ed.look = look;
changed = true;
}
changed |= crate::edit_text_ui::extras_panel(ui, &mut ed.extras);
if changed {
let edit = printcraft_engine::Edit::EditTextBlock { page: ed.page, block: ed.block, text: ed.text.clone(), style: ed.style() };
if app.apply_edit(edit) {
ed.applied();
+7
View File
@@ -689,4 +689,11 @@ fn the_format_panel_restyles_the_paragraph_being_edited() {
assert_eq!(doc.can_undo(), Some("Edit text"));
assert_eq!(doc.text_blocks(0)[0].base_font, "Helvetica-Bold");
assert!(s.views[0].line_editor.is_some(), "still editing");
// Underline: another step, the text keeps its bold.
h.get_by_label("Underline").click();
h.run_steps(4);
let s = h.state();
let doc = s.session.get(s.views[0].id).unwrap();
assert_eq!(doc.text_blocks(0)[0].base_font, "Helvetica-Bold");
assert!(s.views[0].line_editor.as_ref().is_some_and(|e| e.extras.underline));
}
+18 -18
View File
@@ -1839,11 +1839,11 @@ title = "Bold, italic, underline"
area = "C"
tier = "P1"
milestone = "M7"
status = "partial"
commands = ["edit.edit_text", "edit.text"]
status = "shipped"
commands = ["edit.edit_text"]
tools = ["text_edit"]
evidence = ["crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"]
notes = "bold and italic through the standard font families; underline is not done yet"
evidence = ["crates/edit/src/tests.rs::justify_underline_and_spacing", "crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited"]
notes = "bold and italic through the standard families; underline drawn under each line in the text colour"
[[feature]]
id = "edit.text-super-subscript"
@@ -1863,11 +1863,11 @@ title = "Paragraph alignment (left, centre, right, justify)"
area = "C"
tier = "P0"
milestone = "M7"
status = "partial"
status = "shipped"
commands = ["edit.edit_text", "edit.text"]
tools = ["text_edit"]
evidence = ["crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"]
notes = "left, centre, right for existing paragraphs (per wrapped line) and added text; justify is not done yet"
tools = ["text_edit", "content_update"]
evidence = ["crates/edit/src/tests.rs::justify_underline_and_spacing", "crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited"]
notes = "left, centre, right and justify (word spacing per line, last line left) for existing paragraphs and added text"
[[feature]]
id = "edit.line-paragraph-spacing"
@@ -1875,11 +1875,11 @@ title = "Line and paragraph spacing"
area = "C"
tier = "P1"
milestone = "M7"
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
status = "partial"
commands = ["edit.edit_text"]
tools = ["text_edit"]
evidence = ["crates/edit/src/tests.rs::justify_underline_and_spacing", "crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited"]
notes = "line spacing (1, 1.15, 1.5, 2 × size or any multiple); space between paragraphs is not adjustable yet"
[[feature]]
id = "edit.char-spacing-scale"
@@ -1887,11 +1887,11 @@ title = "Character spacing and horizontal scaling"
area = "C"
tier = "P1"
milestone = "M7"
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
status = "shipped"
commands = ["edit.edit_text"]
tools = ["text_edit"]
evidence = ["crates/edit/src/tests.rs::justify_underline_and_spacing", "crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited"]
notes = "character spacing in points and horizontal scaling in percent for a paragraph (Tc, Tz), rewrapped"
[[feature]]
id = "edit.lists"