Watermark: text watermark transparency (#3001)

Ported from #3001 by hand onto current main (the PR was 1857 commits behind).
The text watermark gets a transparency in percent next to Word's Semitransparent
check box: 0 opaque, 50 Semitransparent, any other value kept (`v:fill opacity`).

- doc: `Watermark::transparency` (Option<u8>), `transparency()`, `set_transparency()`,
  `opacity()`; older documents without it follow `semitransparent`.
- DOCX: reads any `v:fill opacity`, writes `.75`, `.2`, ... like Word writes `.5`.
- Render and PDF draw the same opacity. Changed vs the PR's base: the screen used
  35 % for Semitransparent and 80 % for opaque; both now use the real opacity
  (50 % / 100 %), as the PDF already did.
- Command `design.watermark`: `transparency` (0-100, non-finite and out-of-range
  clamped) and `semitransparent`.
- Dialog: Semitransparent check box and a percentage.
- Beyond the PR: the ODT writer and reader keep the transparency (`draw:opacity`)
  instead of only on/off.

Co-Authored-By: C00kieMonsta <22135261+C00kieMonsta@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCVCCEo7u5wetHaJRU4
This commit is contained in:
Martin Ball
2026-11-05 23:54:16 +00:00
co-authored by C00kieMonsta Claude Sonnet 5.5
parent 8c57b0775c
commit 0261e92d10
18 changed files with 246 additions and 25 deletions
+54
View File
@@ -234,7 +234,12 @@ pub struct Watermark {
pub font: String,
pub color: Rgb,
pub diagonal: bool,
/// Word's Semitransparent check box (the text 50 % transparent).
pub semitransparent: bool,
/// The text's transparency in percent when it is neither 0 nor 50 (see
/// [`Watermark::transparency`]); `None` follows `semitransparent`, as older documents do.
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency: Option<u8>,
/// The shape's box (its `style` width and height, points), which the text is stretched to
/// fill (`v:textpath fitshape`); `None`: the box Word's Design › Watermark gives
/// ([`Watermark::frame`]).
@@ -250,12 +255,34 @@ impl Default for Watermark {
color: Rgb(0xC0, 0xC0, 0xC0),
diagonal: true,
semitransparent: true,
transparency: None,
size: None,
}
}
}
impl Watermark {
/// The text's transparency in percent: 0 is opaque, 100 invisible, Semitransparent is 50.
pub fn transparency(&self) -> u8 {
match self.transparency {
Some(t) => t.min(100),
None if self.semitransparent => 50,
None => 0,
}
}
/// Set the text's transparency in percent (at most 100). Semitransparent follows it.
pub fn set_transparency(&mut self, percent: u8) {
let t = percent.min(100);
self.semitransparent = t > 0;
self.transparency = (t != 0 && t != 50).then_some(t);
}
/// The text's opacity, 0 to 1.
pub fn opacity(&self) -> f32 {
1.0 - f32::from(self.transparency()) / 100.0
}
/// The box the text fills on a page `page_w` × `page_h` (points): its own size, else half the
/// page diagonal across (half the page width when not diagonal), as high as the text's length
/// makes it. Centred on the margins and, when diagonal, turned 45° counterclockwise (Word's
@@ -332,3 +359,30 @@ impl Default for Settings {
}
}
}
#[cfg(test)]
mod watermark_tests {
use super::Watermark;
#[test]
fn transparency_follows_semitransparent() {
let mut wm = Watermark::default();
assert_eq!((wm.transparency(), wm.opacity()), (50, 0.5));
wm.semitransparent = false;
assert_eq!(wm.transparency(), 0);
wm.set_transparency(80);
assert_eq!((wm.semitransparent, wm.transparency, wm.transparency()), (true, Some(80), 80));
// Word's two values are stored as the check box alone.
wm.set_transparency(50);
assert_eq!((wm.semitransparent, wm.transparency), (true, None));
wm.set_transparency(0);
assert_eq!((wm.semitransparent, wm.transparency), (false, None));
wm.set_transparency(255);
assert_eq!((wm.transparency(), wm.opacity()), (100, 0.0));
// Older documents have no transparency field.
let old: Watermark = serde_json::from_str(r#"{"text":"X","semitransparent":false}"#).unwrap();
assert_eq!(old.transparency(), 0);
let hostile: Watermark = serde_json::from_str(r#"{"transparency":200}"#).unwrap();
assert_eq!(hostile.transparency(), 100);
}
}
+4 -2
View File
@@ -605,7 +605,7 @@ fn vml_float(shape: &El, style: &str, in_hf: bool) -> Float {
/// Word's text watermark (Design › Watermark): a VML shape in a header whose `v:textpath` draws
/// its `string` as text (ECMA-376 Part 4, VML `textpath`), in the textpath's `font-family`, filled
/// with the shape's `fillcolor`, semitransparent when its `v:fill` has an `opacity` below 1, and
/// with the shape's `fillcolor`, as transparent as its `v:fill` `opacity` says (any value), and
/// diagonal when the shape is turned. Only Word's watermark shape counts (its `id`), or text
/// placed absolutely and centred both ways; other header WordArt (a logo or a title) is None.
fn vml_watermark(shape: &El) -> Option<wordcraft_doc::Watermark> {
@@ -638,7 +638,9 @@ fn vml_watermark(shape: &El) -> Option<wordcraft_doc::Watermark> {
// The box the text is stretched to (`fitshape`).
let len = |k: &str| vml_style(style, k).and_then(|v| measure(v, 1.0)).filter(|v| v.is_finite() && *v > 0.0).map(|v| v.min(4000.0));
let size = len("width").zip(len("height")).map(|(w, h)| [w, h]);
Some(wordcraft_doc::Watermark { text, font, color, diagonal: vml_spin(style).deg != 0.0, semitransparent: opacity < 1.0, size })
let mut wm = wordcraft_doc::Watermark { text, font, color, diagonal: vml_spin(style).deg != 0.0, size, ..default };
wm.set_transparency(((1.0 - opacity) * 100.0).round().clamp(0.0, 100.0) as u8);
Some(wm)
}
/// A VML `opacity`, 0..=1.
+28 -1
View File
@@ -18,6 +18,18 @@ pub(super) fn text(wm: &Watermark) -> Option<String> {
(!t.trim().is_empty()).then_some(t)
}
/// The `v:fill` `opacity` for the text's transparency: Word's `.5` for Semitransparent. None when
/// the text is opaque.
fn opacity(wm: &Watermark) -> Option<String> {
let t = wm.transparency();
if t == 0 {
return None;
}
let o = format!("{:.2}", f32::from(100 - t) / 100.0);
let o = o.trim_end_matches('0').trim_end_matches('.');
Some(o.strip_prefix("0.").map_or_else(|| o.to_string(), |f| format!(".{f}")))
}
/// A `w:r` holding the watermark shape (and its shape type), for the first paragraph of header
/// part number `n`, in its box ([`Watermark::frame`]: its own, or Word's default for the page).
pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option<String> {
@@ -45,7 +57,7 @@ pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option<Strin
mso-position-horizontal:center;mso-position-horizontal-relative:margin;\
mso-position-vertical:center;mso-position-vertical-relative:margin"
);
let fill = if wm.semitransparent { r#"<v:fill opacity=".5"/>"# } else { "" };
let fill = opacity(wm).map(|o| format!(r#"<v:fill opacity="{o}"/>"#)).unwrap_or_default();
Some(format!(
concat!(
r#"<w:r><w:rPr><w:noProof/></w:rPr><w:pict>"#,
@@ -64,3 +76,18 @@ pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option<Strin
text = esc(&text),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opacity_is_written_like_word() {
let mut wm = Watermark::default();
assert_eq!(opacity(&wm).as_deref(), Some(".5"));
for (t, want) in [(0, None), (25, Some(".75")), (80, Some(".2")), (99, Some(".01")), (100, Some("0"))] {
wm.set_transparency(t);
assert_eq!(opacity(&wm).as_deref(), want, "{t}");
}
}
}
+8 -1
View File
@@ -1381,7 +1381,14 @@ fn vml_textpath_in_header_is_the_watermark() {
// Horizontal, centred both ways without Word's id, opacity in 65536ths.
let flat = r##"<v:shape type="#_x0000_t136" style="position:absolute;width:300pt;height:60pt;mso-position-horizontal:center;mso-position-vertical:center" fillcolor="#336699"><v:fill opacity="32768f"/><v:textpath string="ASAP"/></v:shape>"##;
let wm = read(flat, true).0.expect("watermark");
assert_eq!((wm.color, wm.diagonal, wm.semitransparent), (Rgb(0x33, 0x66, 0x99), false, true));
assert_eq!((wm.color, wm.diagonal, wm.semitransparent, wm.transparency()), (Rgb(0x33, 0x66, 0x99), false, true, 50));
// Any opacity is read as a transparency percentage, not only Word's `.5`.
for (opacity, want) in [(".25", 75), ("0", 100), ("1", 0), ("2", 0), ("-1", 100)] {
let shape = flat.replace(r#"opacity="32768f""#, &format!(r#"opacity="{opacity}""#));
let wm = read(&shape, true).0.expect("watermark");
assert_eq!(wm.transparency(), want, "{opacity}");
}
// Not watermarks: the same shape in the body (WordArt); header WordArt that isn't centred
// and isn't Word's watermark (a logo); a textpath turned off; a watermark-like shape inside a
+23
View File
@@ -1561,7 +1561,10 @@ fn watermark_roundtrips() {
diagonal: false,
semitransparent: false,
size: None,
..Default::default()
},
// Any transparency, not only Word's two.
Watermark { transparency: Some(80), ..Default::default() },
// A box of its own (not Word's default for the page) is kept.
Watermark { text: "COPY".into(), size: Some([300.0, 80.0]), ..Default::default() },
] {
@@ -1573,6 +1576,26 @@ fn watermark_roundtrips() {
}
}
/// #2954: the text watermark's transparency round-trips as `v:fill` `opacity`; Word's
/// Semitransparent is `.5`, and an opaque watermark has no `v:fill`.
#[test]
fn text_watermark_transparency_roundtrips() {
for (t, fill) in [(0, None), (25, Some(".75")), (50, Some(".5")), (80, Some(".2")), (100, Some("0"))] {
let mut d = doc_with(vec![Paragraph::with_text("Body", CharProps::default())]);
let mut wm = Watermark::default();
wm.set_transparency(t);
d.settings.watermark = Some(wm.clone());
let bytes = wordcraft_docx::write(&d).unwrap();
let hdr = part_xml(&bytes, "word/header1.xml");
match fill {
Some(o) => assert!(hdr.contains(&format!(r#"<v:fill opacity="{o}"/>"#)), "{t}: {hdr}"),
None => assert!(!hdr.contains("v:fill"), "{t}"),
}
let r = wordcraft_docx::read(&bytes).unwrap();
assert_eq!(r.settings.watermark, Some(wm), "{t}");
}
}
/// #3994: a watermark font name round-trips through the textpath's CSS-like style whatever it
/// carries — apostrophes, semicolons, colons, quotes, backslashes. Before, those characters
/// were deleted (the font was saved under a different name) and a trailing backslash escaped
+12
View File
@@ -17,6 +17,18 @@ fn tiny_png() -> Vec<u8> {
out.into_inner()
}
/// #2954: a text watermark's transparency survives Word 2003 XML (the same VML shape as in DOCX).
#[test]
fn text_watermark_transparency_round_trips_through_2003() {
let mut d = Document::new();
d.body = vec![para_block(Paragraph::with_text("Body", CharProps::default()))];
let mut wm = wordcraft_doc::Watermark::default();
wm.set_transparency(70);
d.settings.watermark = Some(wm.clone());
let back = read_xml(&write_wordml2003(&d).expect("write")).expect("read");
assert_eq!(back.settings.watermark, Some(wm));
}
fn paras(doc: &Document) -> Vec<&Paragraph> {
doc.body.iter().filter_map(|b| b.as_para()).collect()
}
+40 -3
View File
@@ -3,7 +3,7 @@
use serde_json::{Value, json};
use std::sync::Arc;
use wordcraft_doc::props::{Border, BorderStyle, Borders, CharProps, LineSpacing, Rgb, TextColor};
use wordcraft_doc::{Watermark, styles};
use wordcraft_doc::styles;
use super::sel_result;
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
@@ -71,7 +71,7 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("design.scaleFontSizes", "Scale Font Sizes", "Design › Document Formatting", scale_font_sizes)
.params(r#"{"delta": pt (-1638..1638, not 0) | "percent": number (-99..1000, not 0; 10 = +10%)} (exactly one)"#),
CommandSpec::new("design.watermark", "Watermark", "Design › Page Background", watermark)
.params(r#"{"text"?: string, "remove"?: bool, "diagonal"?: bool, "color"?: "RRGGBB"}"#),
.params(r#"{"text"?: string, "remove"?: bool, "diagonal"?: bool, "color"?: "RRGGBB", "transparency"?: 0-100 (percent; Word's Semitransparent is 50), "semitransparent"?: bool (50 or 0 percent)}"#),
CommandSpec::new("design.pageColor", "Page Color", "Design › Page Background", |s, v| {
s.doc.settings.page_color = p::str(v, "color").and_then(Rgb::parse);
s.doc.settings.page_picture = None;
@@ -262,7 +262,14 @@ fn watermark(s: &mut Session, v: &Value) -> CmdResult {
if let Some(c) = p::str(v, "color").and_then(Rgb::parse) {
wm.color = c;
}
s.doc.settings.watermark = Some(Watermark { ..wm });
if let Some(on) = p::bool(v, "semitransparent") {
wm.set_transparency(if on { 50 } else { 0 });
}
// Applied after `semitransparent`, so a percentage wins when both are given.
if let Some(t) = p::f32(v, "transparency") {
wm.set_transparency(if t.is_finite() { t.clamp(0.0, 100.0).round() as u8 } else { 0 });
}
s.doc.settings.watermark = Some(wm);
sel_result(s)
}
@@ -288,3 +295,33 @@ fn page_borders(s: &mut Session, v: &Value) -> CmdResult {
}
sel_result(s)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use wordcraft_doc::{Document, Watermark};
use crate::Session;
fn wm(s: &Session) -> Watermark {
s.doc.settings.watermark.clone().unwrap()
}
/// #2954: the text's transparency in percent, kept in step with Semitransparent.
#[test]
fn text_watermark_transparency() {
let mut s = Session::new(Document::new());
s.run("design.watermark", &json!({"text": "DRAFT"})).unwrap();
assert_eq!((wm(&s).transparency(), wm(&s).semitransparent), (50, true));
s.run("design.watermark", &json!({"transparency": 80})).unwrap();
assert_eq!((wm(&s).transparency(), wm(&s).semitransparent), (80, true));
s.run("design.watermark", &json!({"semitransparent": false})).unwrap();
assert_eq!((wm(&s).transparency(), wm(&s).semitransparent), (0, false));
s.run("design.watermark", &json!({"transparency": 250.7})).unwrap();
assert_eq!(wm(&s).transparency(), 100);
s.run("design.watermark", &json!({"transparency": -3})).unwrap();
assert_eq!(wm(&s).transparency(), 0);
s.run("edit.undo", &json!({})).unwrap();
assert_eq!(wm(&s).transparency(), 100);
}
}
+2 -2
View File
@@ -63,8 +63,8 @@ pub(super) struct Body<'a> {
/// A watermark found (in a header): see [`Self::start_shape`].
pub(super) watermark: Option<wordcraft_doc::Watermark>,
/// The shape being read is a watermark's, if its geometry follows its text: its size, turn,
/// colour and see-through, and whether its geometry is fontwork.
watermark_shape: Option<((f32, f32), f32, Option<wordcraft_doc::props::Rgb>, bool, bool)>,
/// colour and transparency (percent), and whether its geometry is fontwork.
watermark_shape: Option<((f32, f32), f32, Option<wordcraft_doc::props::Rgb>, u8, bool)>,
pub(super) manifest: Option<&'a HashMap<String, String>>,
containers: Vec<Vec<FBlock>>,
/// Drawings anchored in the text but outside any paragraph (LibreOffice's page-anchored
+8 -6
View File
@@ -176,7 +176,7 @@ impl Body<'_> {
/// The shape ending is a watermark (see [`Self::start_shape`]) holding `blocks`: the
/// document's watermark, from its text, font, colour, size and turn, and not a shape.
fn take_watermark(&mut self, blocks: &[FBlock]) -> bool {
let Some(((w, h), rot, color, see_through, true)) = self.watermark_shape.take() else { return false };
let Some(((w, h), rot, color, transparency, true)) = self.watermark_shape.take() else { return false };
let mut text = String::new();
let mut font = None;
for b in blocks {
@@ -195,14 +195,16 @@ impl Body<'_> {
}
let turned = rot.rem_euclid(360.0);
let base = wordcraft_doc::Watermark::default();
self.watermark = Some(wordcraft_doc::Watermark {
let mut wm = wordcraft_doc::Watermark {
text,
font: font.unwrap_or(base.font),
color: color.unwrap_or(base.color),
diagonal: turned > 1.0 && turned < 359.0,
semitransparent: see_through,
size: Some([w, h]).filter(|[w, h]| w.is_finite() && h.is_finite() && *w > 0.0 && *h > 0.0),
});
..base
};
wm.set_transparency(transparency);
self.watermark = Some(wm);
true
}
@@ -372,8 +374,8 @@ impl Body<'_> {
self.frame_look.conn = self.conn_of(a).map(Box::new);
// Word's and LibreOffice's watermark: a shape of this name whose geometry follows its text.
self.watermark_shape = get(a, "draw:name").filter(|n| n.starts_with(crate::odt::writer::WATERMARK_NAME)).map(|_| {
let (color, see_through) = g.watermark_look();
((w, h), float.rot, color, see_through, false)
let (color, transparency) = g.watermark_look();
((w, h), float.rot, color, transparency, false)
});
self.shape_path = crate::odt::shapes::freeform(local(q), a, w, h).map(std::sync::Arc::new);
self.shape_kind = Some(kind);
+3 -3
View File
@@ -181,9 +181,9 @@ impl Graphic {
}
}
/// A watermark's look from its shape's style: its fill colour, and whether it is see-through.
pub(super) fn watermark_look(&self) -> (Option<Rgb>, bool) {
(self.fill_color, self.opacity.is_some_and(|o| o < 100.0))
/// A watermark's look from its shape's style: its fill colour, and its transparency in percent.
pub(super) fn watermark_look(&self) -> (Option<Rgb>, u8) {
(self.fill_color, self.opacity.filter(|o| o.is_finite()).map_or(0, |o| (100.0 - o).round().clamp(0.0, 100.0) as u8))
}
/// This placement with `parent`'s filling what it leaves unsaid.
@@ -272,6 +272,15 @@ fn a_watermark_is_a_shape_in_the_header_and_reads_back_as_one() {
let back = import(&saved).unwrap();
let wm = back.settings.watermark.clone().expect("the watermark");
assert_eq!((wm.text.as_str(), wm.diagonal, wm.semitransparent), ("CONFIDENTIAL", true, true));
// Any transparency is kept, not only half.
for t in [0u8, 30, 50, 80] {
let mut other = d.clone();
let mut w = wordcraft_doc::Watermark { text: "CONFIDENTIAL".into(), ..Default::default() };
w.set_transparency(t);
other.settings.watermark = Some(w.clone());
let back = import(&export(&other).unwrap()).unwrap();
assert_eq!(back.settings.watermark.map(|w| w.transparency()), Some(t), "{t}");
}
// Not a shape in the header too.
let header = back.last_section.headers.default.and_then(|id| back.parts.get(&id)).unwrap();
assert!(header.blocks.iter().filter_map(|b| b.as_para()).all(|p| p.objects.is_empty()));
+5 -2
View File
@@ -603,11 +603,14 @@ impl Writer {
/// The document's watermark as LibreOffice and Word write one in a header: a fontwork shape
/// (§10.3.2 `draw:custom-shape` whose geometry follows its text, `draw:text-path`) behind the
/// text, centred on a page whose text area is `area` (width, height; points), turned up 45°
/// when diagonal, filled with its colour (half transparent when semitransparent). It doesn't
/// when diagonal, filled with its colour (as transparent as the watermark says). It doesn't
/// grow to its text: its text is stretched to it.
pub(in crate::odt) fn watermark(&mut self, wm: &wordcraft_doc::Watermark, page: (f32, f32), area: (f32, f32)) -> String {
let [w, h] = wm.frame(page.0, page.1);
let opacity = if wm.semitransparent { " draw:opacity=\"50%\"" } else { "" };
let opacity = match wm.transparency() {
0 => String::new(),
t => format!(" draw:opacity=\"{}%\"", 100 - t),
};
let style = self.graphic_style(format!(
" draw:stroke=\"none\" draw:fill=\"solid\" draw:fill-color=\"#{}\"{opacity} style:run-through=\"background\" style:wrap=\"run-through\" style:horizontal-pos=\"center\" style:horizontal-rel=\"page-content\" style:vertical-pos=\"middle\" style:vertical-rel=\"page-content\" style:flow-with-text=\"false\" draw:auto-grow-height=\"false\" draw:auto-grow-width=\"false\"",
wm.color.hex().to_ascii_lowercase()
+1 -1
View File
@@ -374,7 +374,7 @@ impl Exporter<'_> {
.collect();
s.push_transform(&Transform::from_row(c * stretch, sn * stretch, -sn, c, c * dx - sn * dy + cx, sn * dx + c * dy + cy));
s.set_stroke(None);
s.set_fill(Some(fill(wm.color, if wm.semitransparent { 0.5 } else { 1.0 })));
s.set_fill(Some(fill(wm.color, wm.opacity())));
match font {
Some(font) => {
s.draw_glyphs(Point::from_xy(0.0, 0.0), &glyphs, font, &text, size, false);
+1 -1
View File
@@ -480,7 +480,7 @@ fn draw_watermark(ctx: &mut RenderContext, view: Affine, page: &Page, wm: &wordc
* Affine::rotate(angle)
* Affine::translate((-bw / 2.0, bh / 2.0 - descent * bh))
* Affine::scale_non_uniform(stretch, 1.0);
ctx.set_paint(color(opts.ink(wm.color), if wm.semitransparent { 0.35 } else { 0.8 }));
ctx.set_paint(color(opts.ink(wm.color), wm.opacity()));
let db = FontDb::global();
let mut x = 0.0;
for g in &glyphs {
+1
View File
@@ -3,6 +3,7 @@ use wordcraft_layout::display::page_display;
use wordcraft_layout::{LayoutCache, LayoutOptions, layout};
mod color_glyphs;
mod watermark;
/// The first and last pixel rows with ink in a black-on-white [`render_text_line`] raster.
fn ink_rows(img: &Rendered) -> Option<(usize, usize)> {
+24
View File
@@ -0,0 +1,24 @@
//! Design › Watermark on screen.
use super::*;
/// #2954: a text watermark at 80 % transparency is lighter than at 0 %, and invisible at 100 %.
#[test]
fn text_watermark_transparency() {
let mut d = Document::new();
let darkest = |d: &Document| {
let l = layout(d, &mut LayoutCache::new(), &LayoutOptions::default());
let img = render_page(d, &l.pages[0], 1.0, &RenderOptions::default());
img.pixels.chunks(4).map(|p| u32::from(p[0]) + u32::from(p[1]) + u32::from(p[2])).min().unwrap()
};
let mut wm = wordcraft_doc::Watermark { text: "WWW".into(), color: wordcraft_doc::props::Rgb::BLACK, diagonal: false, ..Default::default() };
let mut at = |t: u8| {
wm.set_transparency(t);
d.settings.watermark = Some(wm.clone());
darkest(&d)
};
let (opaque, half, light, gone) = (at(0), at(50), at(80), at(100));
assert!(opaque < 30, "{opaque}");
assert!(half > opaque + 300 && light > half + 150, "{opaque} {half} {light}");
assert_eq!(gone, 765);
}
+13 -1
View File
@@ -218,6 +218,8 @@ pub enum Dialog {
Watermark {
text: String,
diagonal: bool,
/// Percent, 0 to 100; Semitransparent is 50.
transparency: u8,
},
/// Home › Font › Phonetic Guide: ruby text over the selected text (`format.phonetic`).
Phonetic {
@@ -731,7 +733,17 @@ impl Dialog {
"bookmark" => Dialog::Bookmark { name: bookmark_at_caret(&app.session), by_location: false, hidden: false, deleted: false },
"wordCount" => Dialog::WordCount { stats: app.session.run("review.wordCount", &json!({})).unwrap_or_default() },
"zoom" => Dialog::Zoom { percent: (app.session.view.zoom * 100.0).round() },
"watermark" => Dialog::Watermark { text: "CONFIDENTIAL".into(), diagonal: true },
"watermark" => {
// The document's watermark when it has one, else Word's defaults.
let wm = app
.session
.doc
.settings
.watermark
.clone()
.unwrap_or_else(|| wordcraft_doc::Watermark { text: "CONFIDENTIAL".into(), ..Default::default() });
Dialog::Watermark { transparency: wm.transparency(), diagonal: wm.diagonal, text: wm.text }
}
"phonetic" => {
let v = wordcraft_engine::cmd::phonetic::state(&app.session);
let f = |k: &str| v.get(k).and_then(Value::as_f64).unwrap_or(0.0) as f32;
+10 -2
View File
@@ -871,20 +871,28 @@ fn zoom_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
fn watermark_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let Dialog::Watermark { text, diagonal } = d else {
let Dialog::Watermark { text, diagonal, transparency } = d else {
return false;
};
ui.horizontal(|ui| {
ui.label(tl!("Text:"));
ui.text_edit_singleline(text);
});
ui.horizontal(|ui| {
let mut semi = *transparency > 0;
if ui.checkbox(&mut semi, tl!("Semitransparent")).changed() {
*transparency = if semi { 50 } else { 0 };
}
ui.label(tl!("Transparency:"));
ui.add(egui::DragValue::new(transparency).range(0..=100).suffix(" %"));
});
ui.horizontal(|ui| {
ui.radio_value(diagonal, true, tl!("Diagonal"));
ui.radio_value(diagonal, false, tl!("Horizontal"));
});
let (ok, cancel) = buttons(ui, tl!("OK"));
if ok {
let _ = app.run("design.watermark", json!({"text": text, "diagonal": *diagonal}));
let _ = app.run("design.watermark", json!({"text": text, "diagonal": *diagonal, "transparency": *transparency}));
}
ok || cancel
}