diff --git a/crates/doc/src/settings.rs b/crates/doc/src/settings.rs index 3dd38116c7..8aceae3368 100644 --- a/crates/doc/src/settings.rs +++ b/crates/doc/src/settings.rs @@ -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, /// 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); + } +} diff --git a/crates/docx/src/read/story/vml.rs b/crates/docx/src/read/story/vml.rs index b625b8d4f0..f93860c86f 100644 --- a/crates/docx/src/read/story/vml.rs +++ b/crates/docx/src/read/story/vml.rs @@ -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 { @@ -638,7 +638,9 @@ fn vml_watermark(shape: &El) -> Option { // 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. diff --git a/crates/docx/src/write/watermark.rs b/crates/docx/src/write/watermark.rs index 998227e6aa..2ab13efbe6 100644 --- a/crates/docx/src/write/watermark.rs +++ b/crates/docx/src/write/watermark.rs @@ -18,6 +18,18 @@ pub(super) fn text(wm: &Watermark) -> Option { (!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 { + 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 { @@ -45,7 +57,7 @@ pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option"# } else { "" }; + let fill = opacity(wm).map(|o| format!(r#""#)).unwrap_or_default(); Some(format!( concat!( r#""#, @@ -64,3 +76,18 @@ pub(super) fn run(wm: &Watermark, n: usize, page: &SectionProps) -> Option"##; 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 diff --git a/crates/docx/tests/it/roundtrip.rs b/crates/docx/tests/it/roundtrip.rs index 9738fd3724..72bcaf2342 100644 --- a/crates/docx/tests/it/roundtrip.rs +++ b/crates/docx/tests/it/roundtrip.rs @@ -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#""#)), "{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 diff --git a/crates/docx/tests/it/wordml2003_write.rs b/crates/docx/tests/it/wordml2003_write.rs index 0db8455494..28543a35dc 100644 --- a/crates/docx/tests/it/wordml2003_write.rs +++ b/crates/docx/tests/it/wordml2003_write.rs @@ -17,6 +17,18 @@ fn tiny_png() -> Vec { 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() } diff --git a/crates/engine/src/cmd/design.rs b/crates/engine/src/cmd/design.rs index 0e091c9f47..f8b26f49a2 100644 --- a/crates/engine/src/cmd/design.rs +++ b/crates/engine/src/cmd/design.rs @@ -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::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); + } +} diff --git a/crates/formats/src/odt/body.rs b/crates/formats/src/odt/body.rs index 731c94ba9d..4b1dca91ad 100644 --- a/crates/formats/src/odt/body.rs +++ b/crates/formats/src/odt/body.rs @@ -63,8 +63,8 @@ pub(super) struct Body<'a> { /// A watermark found (in a header): see [`Self::start_shape`]. pub(super) watermark: Option, /// 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, bool, bool)>, + /// colour and transparency (percent), and whether its geometry is fontwork. + watermark_shape: Option<((f32, f32), f32, Option, u8, bool)>, pub(super) manifest: Option<&'a HashMap>, containers: Vec>, /// Drawings anchored in the text but outside any paragraph (LibreOffice's page-anchored diff --git a/crates/formats/src/odt/body/drawing.rs b/crates/formats/src/odt/body/drawing.rs index e3701bd9b5..54044dc152 100644 --- a/crates/formats/src/odt/body/drawing.rs +++ b/crates/formats/src/odt/body/drawing.rs @@ -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); diff --git a/crates/formats/src/odt/frames.rs b/crates/formats/src/odt/frames.rs index 6bb23da1f5..9f37e39118 100644 --- a/crates/formats/src/odt/frames.rs +++ b/crates/formats/src/odt/frames.rs @@ -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, 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, 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. diff --git a/crates/formats/src/odt/tests_more/headers.rs b/crates/formats/src/odt/tests_more/headers.rs index 57ff01970a..9c5d6dd39d 100644 --- a/crates/formats/src/odt/tests_more/headers.rs +++ b/crates/formats/src/odt/tests_more/headers.rs @@ -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())); diff --git a/crates/formats/src/odt/writer/drawing.rs b/crates/formats/src/odt/writer/drawing.rs index 174be40df3..c01db5232c 100644 --- a/crates/formats/src/odt/writer/drawing.rs +++ b/crates/formats/src/odt/writer/drawing.rs @@ -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() diff --git a/crates/pdf/src/exporter.rs b/crates/pdf/src/exporter.rs index 9ce20c8981..3b2bba72b8 100644 --- a/crates/pdf/src/exporter.rs +++ b/crates/pdf/src/exporter.rs @@ -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); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index c21f88063e..80480d3b49 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -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 { diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index db5e5b0ccf..c80eac0d22 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -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)> { diff --git a/crates/render/src/tests/watermark.rs b/crates/render/src/tests/watermark.rs new file mode 100644 index 0000000000..2b45f941ea --- /dev/null +++ b/crates/render/src/tests/watermark.rs @@ -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); +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 8f83ffef89..bc01b458f5 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -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; diff --git a/crates/ui-egui/src/dialogs/dispatch.rs b/crates/ui-egui/src/dialogs/dispatch.rs index 77344875f9..d8966106c9 100644 --- a/crates/ui-egui/src/dialogs/dispatch.rs +++ b/crates/ui-egui/src/dialogs/dispatch.rs @@ -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 }