mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-05 09:49:55 -06:00
Read Mode: ink and drag previews follow the Page color, which is kept between entries (#4228)
This commit is contained in:
@@ -177,11 +177,11 @@ fn full_screen_available(_: &Session) -> Option<&'static str> {
|
||||
|
||||
fn mode(s: &mut Session, m: ViewMode, read: bool) -> CmdResult {
|
||||
if read && !s.view.read_mode {
|
||||
// Reading controls are session-only, and reset on each entry to the view.
|
||||
// Width, size and spacing reset on each entry. Page Color follows the saved preference.
|
||||
s.view.read_column_width = None;
|
||||
s.view.read_zoom = 1.0;
|
||||
s.view.read_line_spacing = None;
|
||||
s.view.read_page_color = ReadPageColor::None;
|
||||
s.view.read_page_color = s.prefs.read_page_color;
|
||||
}
|
||||
s.view.mode = m;
|
||||
s.view.read_mode = read;
|
||||
@@ -256,6 +256,7 @@ fn read_page_color(s: &mut Session, v: &Value) -> CmdResult {
|
||||
_ => return Err(CmdError::Params("reading page color must be none, sepia, or inverse".into())),
|
||||
};
|
||||
s.view.read_page_color = color;
|
||||
s.prefs.read_page_color = color;
|
||||
Ok(json!({"pageColor": color}))
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ pub struct Prefs {
|
||||
/// Check spelling as you type (File › Options › Proofing). The live value is
|
||||
/// `ViewState::proofing`; this is the copy that is saved and restored (#1792).
|
||||
pub check_spelling: bool,
|
||||
/// Read Mode's Page Color. The live value is `ViewState::read_page_color`.
|
||||
#[serde(deserialize_with = "lenient_read_page_color")]
|
||||
pub read_page_color: crate::cmd::view::ReadPageColor,
|
||||
pub ignore_uppercase: bool,
|
||||
pub ignore_numbers: bool,
|
||||
pub ignore_internet: bool,
|
||||
@@ -74,6 +77,7 @@ impl Default for Prefs {
|
||||
marks: Default::default(),
|
||||
mark_grammar: true,
|
||||
check_spelling: true,
|
||||
read_page_color: Default::default(),
|
||||
ignore_uppercase: true,
|
||||
ignore_numbers: true,
|
||||
ignore_internet: true,
|
||||
@@ -100,3 +104,30 @@ fn lenient_default_language<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Op
|
||||
let value = serde_json::Value::deserialize(d)?;
|
||||
Ok(value.as_str().and_then(wordcraft_proof::lang::normalize_tag))
|
||||
}
|
||||
|
||||
/// Ignore an invalid saved Page Color without discarding other preferences.
|
||||
fn lenient_read_page_color<'de, D: serde::Deserializer<'de>>(d: D) -> Result<crate::cmd::view::ReadPageColor, D::Error> {
|
||||
let value = serde_json::Value::deserialize(d)?;
|
||||
Ok(serde_json::from_value(value).unwrap_or_default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Prefs;
|
||||
use crate::cmd::view::ReadPageColor;
|
||||
|
||||
#[test]
|
||||
fn read_page_color_loads_from_old_and_damaged_prefs() {
|
||||
let old: Prefs = serde_json::from_str(r#"{"checkSpelling":false}"#).unwrap();
|
||||
assert_eq!(old.read_page_color, ReadPageColor::None);
|
||||
assert!(!old.check_spelling);
|
||||
for bad in [r#"{"readPageColor":"unknown","checkSpelling":false}"#, r#"{"readPageColor":42,"checkSpelling":false}"#] {
|
||||
let prefs: Prefs = serde_json::from_str(bad).unwrap();
|
||||
assert_eq!(prefs.read_page_color, ReadPageColor::None);
|
||||
assert!(!prefs.check_spelling);
|
||||
}
|
||||
let saved: Prefs = serde_json::from_str(r#"{"readPageColor":"sepia"}"#).unwrap();
|
||||
assert_eq!(saved.read_page_color, ReadPageColor::Sepia);
|
||||
assert_eq!(serde_json::to_value(saved).unwrap()["readPageColor"], "sepia");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ impl Session {
|
||||
let at = self.others.iter().position(|d| d.document_id == id).ok_or_else(|| unknown(id))?;
|
||||
let mut d = self.others.swap_remove(at);
|
||||
self.swap_doc(&mut d);
|
||||
self.view.read_page_color = self.prefs.read_page_color;
|
||||
self.others.push(d);
|
||||
Ok(())
|
||||
}
|
||||
@@ -459,6 +460,7 @@ impl Session {
|
||||
doc.ensure_nonempty();
|
||||
let mut view = self.view.clone();
|
||||
view.outline.collapsed.clear();
|
||||
view.read_page_color = self.prefs.read_page_color;
|
||||
let mut d = DocState::new(doc, untitled, view);
|
||||
let id = d.document_id;
|
||||
self.order.push(id);
|
||||
|
||||
@@ -31,6 +31,7 @@ impl Session {
|
||||
self.hf_return = None;
|
||||
self.go_back_step = 0;
|
||||
self.view.outline.collapsed.clear();
|
||||
self.view.read_page_color = self.prefs.read_page_color;
|
||||
// A copy the replaced document kept stays in the recovery list.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
|
||||
@@ -135,6 +135,7 @@ impl Session {
|
||||
let current = document_id == self.document_id && rev == self.rev;
|
||||
self.sel = sel;
|
||||
self.view = view;
|
||||
self.view.read_page_color = self.prefs.read_page_color;
|
||||
self.pending = pending;
|
||||
self.goal_x = goal_x;
|
||||
self.page_hint = page_hint;
|
||||
|
||||
@@ -6560,7 +6560,7 @@ fn reading_mode_reflows_per_viewport_without_changing_document_or_export() {
|
||||
}
|
||||
|
||||
/// Read Mode › View › Page Color: None, Sepia or Inverse, a view setting that leaves the
|
||||
/// document alone, is not saved, and starts at None on each entry to Read Mode.
|
||||
/// document alone, starts at None and is kept between documents and runs as a preference.
|
||||
#[test]
|
||||
fn reading_page_color_is_a_transient_view_setting() {
|
||||
use crate::cmd::view::ReadPageColor;
|
||||
@@ -6571,18 +6571,46 @@ fn reading_page_color_is_a_transient_view_setting() {
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::None);
|
||||
assert_eq!(s.run("view.readPageColor", &json!({"value": "sepia"})).unwrap(), json!({"pageColor": "sepia"}));
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Sepia);
|
||||
assert_eq!(s.prefs.read_page_color, ReadPageColor::Sepia);
|
||||
run(&mut s, "view.readPageColor", json!({"value": "inverse"}));
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Inverse);
|
||||
assert_eq!(s.prefs.read_page_color, ReadPageColor::Inverse);
|
||||
for bad in [json!({"value": "black"}), json!({"value": 1}), json!({})] {
|
||||
assert!(s.run("view.readPageColor", &bad).is_err(), "{bad}");
|
||||
}
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Inverse, "rejected values keep the color");
|
||||
assert_eq!(s.prefs.read_page_color, ReadPageColor::Inverse);
|
||||
assert!(serde_json::to_value(&s.view).unwrap().get("readPageColor").is_none());
|
||||
assert_eq!(s.doc, original);
|
||||
assert_eq!(s.dirty, dirty);
|
||||
// The color stays for the next entry to Read Mode, while the other reading controls reset.
|
||||
run(&mut s, "view.readTextSize", json!({"value": 150}));
|
||||
run(&mut s, "view.printLayout", json!({}));
|
||||
run(&mut s, "view.readMode", json!({}));
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::None);
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Inverse);
|
||||
assert_eq!(s.view.read_zoom, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_page_color_follows_saved_prefs_and_new_documents() {
|
||||
use crate::cmd::view::ReadPageColor;
|
||||
let mut s = s();
|
||||
s.prefs.read_page_color = ReadPageColor::Sepia;
|
||||
run(&mut s, "view.readMode", json!({}));
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Sepia);
|
||||
let first = s.document_id();
|
||||
s.view.read_page_color = ReadPageColor::None;
|
||||
run(&mut s, "file.new", json!({}));
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Sepia);
|
||||
run(&mut s, "view.readPageColor", json!({"value": "inverse"}));
|
||||
s.activate(first).unwrap();
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Inverse);
|
||||
s.view.read_page_color = ReadPageColor::None;
|
||||
run(&mut s, "file.open", json!({"path": "reading-color.docx", "data": ""}));
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Inverse);
|
||||
s.view.read_page_color = ReadPageColor::None;
|
||||
s.set_document(wordcraft_doc::Document::new());
|
||||
assert_eq!(s.view.read_page_color, ReadPageColor::Inverse);
|
||||
}
|
||||
|
||||
/// File › Options › Advanced › Numeral: the display mode for digits (display only).
|
||||
|
||||
@@ -61,7 +61,7 @@ pub struct ViewState {
|
||||
pub read_width: f32,
|
||||
#[serde(skip, default = "default_read_height")]
|
||||
pub read_height: f32,
|
||||
/// Reader-only layout controls; they are neither document formatting nor saved UI prefs.
|
||||
/// Reader-only width, size and spacing controls are neither document formatting nor saved UI prefs.
|
||||
/// `None` chooses responsive column width and keeps the document's line spacing.
|
||||
#[serde(skip)]
|
||||
pub read_column_width: Option<f32>,
|
||||
@@ -69,7 +69,7 @@ pub struct ViewState {
|
||||
pub read_zoom: f32,
|
||||
#[serde(skip)]
|
||||
pub read_line_spacing: Option<f32>,
|
||||
/// Read Mode's Page Color: the paper the reading columns are drawn on.
|
||||
/// Read Mode's Page Color: the paper the reading columns are drawn on. `Prefs` saves its value.
|
||||
#[serde(skip)]
|
||||
pub read_page_color: crate::cmd::view::ReadPageColor,
|
||||
pub dark_mode: bool,
|
||||
|
||||
@@ -186,6 +186,12 @@ impl RenderOptions {
|
||||
self.ink(self.read_paper.or(doc.settings.page_color).unwrap_or(self.paper))
|
||||
}
|
||||
|
||||
/// The on-screen color of paper with no page color: [`RenderOptions::read_paper`], else
|
||||
/// [`RenderOptions::paper`]. White Read Mode paper on a dark page is the dark paper grey.
|
||||
pub fn blank_paper(&self) -> Rgb {
|
||||
self.ink(self.read_paper.unwrap_or(self.paper))
|
||||
}
|
||||
|
||||
/// The on-screen colour for a document colour (see [`RenderOptions::dark`]).
|
||||
pub fn ink(&self, c: Rgb) -> Rgb {
|
||||
if !self.dark {
|
||||
|
||||
@@ -350,6 +350,26 @@ fn read_paper_replaces_the_page_background() {
|
||||
assert!(darkest(&plain) < 0x40 && darkest(&on_sepia) <= darkest(&plain) + 8, "{} {}", darkest(&plain), darkest(&on_sepia));
|
||||
}
|
||||
|
||||
/// The paper the canvas draws under a page before its raster: white, the reading paper, or
|
||||
/// the dark paper grey on a dark page, never the document's page color.
|
||||
#[test]
|
||||
fn blank_paper_is_the_reading_paper_or_white_on_screen() {
|
||||
let mut d = Document::from_text("Hello");
|
||||
d.settings.page_color = Some(Rgb(0x10, 0x10, 0x40));
|
||||
let grey = Rgb(DARK_PAPER, DARK_PAPER, DARK_PAPER);
|
||||
for (opts, paper) in [
|
||||
(RenderOptions::default(), Rgb::WHITE),
|
||||
(RenderOptions { read_paper: Some(SEPIA_PAPER), ..Default::default() }, SEPIA_PAPER),
|
||||
(RenderOptions { read_paper: Some(Rgb::WHITE), dark: true, ..Default::default() }, grey),
|
||||
(RenderOptions { dark: true, ..Default::default() }, grey),
|
||||
] {
|
||||
assert_eq!(opts.blank_paper(), paper);
|
||||
if opts.read_paper.is_some() {
|
||||
assert_eq!(opts.paper_of(&d), paper, "the reading paper hides the page color");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A PNG signature, an IHDR declaring `w`×`h` 8-bit RGBA and a short IDAT (all CRCs valid, so
|
||||
/// the header parses), with no real pixel data.
|
||||
fn ihdr_only_png(w: u32, h: u32) -> Vec<u8> {
|
||||
|
||||
@@ -63,6 +63,7 @@ impl WordApp {
|
||||
self.session.prefs = std::mem::take(&mut self.ui.editing);
|
||||
// Check spelling as you type follows the saved preference (#1792).
|
||||
self.session.view.proofing = self.session.prefs.check_spelling;
|
||||
self.session.view.read_page_color = self.session.prefs.read_page_color;
|
||||
// The saved dictionaries folder (#1063) is searched from the start.
|
||||
if let Some(f) = self.session.prefs.dictionaries_folder.as_deref() {
|
||||
wordcraft_engine::cmd::options::use_dictionaries_folder(Some(f));
|
||||
@@ -73,6 +74,7 @@ impl WordApp {
|
||||
self.session.view.nav_pane = self.ui.nav_pane;
|
||||
self.session.view.styles_pane_dock = self.ui.styles_pane_dock;
|
||||
self.session.view.proofing = self.ui.check_spelling;
|
||||
self.session.prefs.check_spelling = self.ui.check_spelling;
|
||||
// Keys for commands this version doesn't have are dropped.
|
||||
self.session.keymap = std::mem::take(&mut self.ui.keyboard);
|
||||
let reg = self.session.registry.clone();
|
||||
|
||||
@@ -60,7 +60,7 @@ pub use navigation::{nearest_page, page_at, page_to_screen};
|
||||
use navigation::{scrollbar_page_tip, viewed_page};
|
||||
#[cfg(test)]
|
||||
use pages::{DRAWING_GRID_MAX_LINES, DRAWING_GRID_MIN_PX};
|
||||
pub(crate) use pages::{PageKeys, page_doc, page_screen_scale, preview_sel, screen_render_options};
|
||||
pub(crate) use pages::{PageKeys, dark_page, page_doc, page_screen_scale, preview_sel, screen_render_options};
|
||||
use pages::{
|
||||
drawing_grid, drawing_grid_step, gridline_cells, page_image, page_key, page_render_options, shown_layout, texel_aligned_rect, view_key,
|
||||
visible_tiles,
|
||||
@@ -484,8 +484,7 @@ struct CanvasView {
|
||||
area: Rect,
|
||||
hruler: Option<Rect>,
|
||||
vruler: Option<Rect>,
|
||||
dark_page: bool,
|
||||
sepia_page: bool,
|
||||
paper: Color32,
|
||||
animate: bool,
|
||||
opts: egui::InputOptions,
|
||||
geo: Geometry,
|
||||
@@ -495,10 +494,13 @@ struct CanvasView {
|
||||
}
|
||||
|
||||
fn prepare_view(app: &mut WordApp, ui: &Ui, full: Rect, layout: &DocLayout) -> CanvasView {
|
||||
use wordcraft_engine::cmd::view::ReadPageColor;
|
||||
let read_mode = app.session.view.read_mode;
|
||||
let dark_page = if read_mode { app.session.view.read_page_color == ReadPageColor::Inverse } else { app.session.view.dark_mode };
|
||||
let sepia_page = read_mode && app.session.view.read_page_color == ReadPageColor::Sepia;
|
||||
// The paper under each page, as the page raster draws it without a page color: the dark page
|
||||
// of View › Switch Modes, or in Read Mode the Page color's white, sepia or dark paper.
|
||||
let paper = {
|
||||
let p = page_render_options(app, false).blank_paper();
|
||||
Color32::from_rgb(p.0, p.1, p.2)
|
||||
};
|
||||
let show_ruler = app.session.view.ruler
|
||||
&& app.session.view.mode == wordcraft_layout::ViewMode::Print
|
||||
&& !read_mode
|
||||
@@ -566,7 +568,7 @@ fn prepare_view(app: &mut WordApp, ui: &Ui, full: Rect, layout: &DocLayout) -> C
|
||||
{
|
||||
app.session.page_hint = c.page;
|
||||
}
|
||||
CanvasView { area, hruler, vruler, dark_page, sepia_page, animate, opts, geo, dim_body, grid_story, caret }
|
||||
CanvasView { area, hruler, vruler, paper, animate, opts, geo, dim_body, grid_story, caret }
|
||||
}
|
||||
|
||||
fn update_caret_scroll(app: &mut WordApp, view: &CanvasView) -> Option<Rect> {
|
||||
@@ -830,8 +832,7 @@ struct PagePaint<'a> {
|
||||
t: &'a Tokens,
|
||||
painter: &'a egui::Painter,
|
||||
dim_body: bool,
|
||||
dark_page: bool,
|
||||
sepia_page: bool,
|
||||
paper: Color32,
|
||||
hidden: Option<(usize, Pos)>,
|
||||
view: u64,
|
||||
content_origin: Pos2,
|
||||
@@ -878,8 +879,7 @@ fn draw_page_gridlines(app: &WordApp, painter: &egui::Painter, t: &Tokens, page:
|
||||
}
|
||||
|
||||
fn paint_pages(pass: PagePaint<'_>) -> PagePaintOutput {
|
||||
let PagePaint { app, ui, layout, geo, t, painter, dim_body, dark_page, sepia_page, hidden, view, content_origin, ppp, max_tex, grid_story } =
|
||||
pass;
|
||||
let PagePaint { app, ui, layout, geo, t, painter, dim_body, paper, hidden, view, content_origin, ppp, max_tex, grid_story } = pass;
|
||||
let mut visible_tile_keys = HashSet::new();
|
||||
let mut rendered = 0;
|
||||
let mut rects = Vec::with_capacity(geo.rects.len());
|
||||
@@ -942,14 +942,6 @@ fn paint_pages(pass: PagePaint<'_>) -> PagePaintOutput {
|
||||
if band_top <= 0.0 && band_bot >= sr.height() { painter.clone() } else { painter.with_clip_rect(painter.clip_rect().intersect(band)) };
|
||||
// Shadow and paper.
|
||||
painter.rect_filled(visual_sr.translate(vec2(0.0, 2.0)).expand(1.5), 1.0, t.page_shadow);
|
||||
let paper = if dark_page {
|
||||
Color32::from_gray(wordcraft_render::DARK_PAPER)
|
||||
} else if sepia_page {
|
||||
let s = wordcraft_render::SEPIA_PAPER;
|
||||
Color32::from_rgb(s.0, s.1, s.2)
|
||||
} else {
|
||||
Color32::WHITE
|
||||
};
|
||||
painter.rect_filled(visual_sr, 0.0, paper);
|
||||
if tiled {
|
||||
// Render before painting to keep the frame sharp immediately after an edit or
|
||||
@@ -1319,7 +1311,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
let scroll_target = update_caret_scroll(app, &view);
|
||||
let motion = scroll_motion(app, ui, full, &view);
|
||||
let scroll_area = configure_scroll_area(app, ui, &view, motion);
|
||||
let CanvasView { area, hruler, vruler, dark_page, sepia_page, geo, dim_body, grid_story, caret, .. } = view;
|
||||
let CanvasView { area, hruler, vruler, paper, geo, dim_body, grid_story, caret, .. } = view;
|
||||
let mut origin = area.min;
|
||||
let mut ui_area = ui.new_child(egui::UiBuilder::new().max_rect(area));
|
||||
let mut ime_geometry: Option<(Rect, Rect)> = None;
|
||||
@@ -1346,8 +1338,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
t: &t,
|
||||
painter: &painter,
|
||||
dim_body,
|
||||
dark_page,
|
||||
sepia_page,
|
||||
paper,
|
||||
hidden,
|
||||
view: page_view,
|
||||
content_origin,
|
||||
|
||||
@@ -221,6 +221,14 @@ pub(crate) fn preview_sel(app: &WordApp) -> Option<&wordcraft_engine::Selection>
|
||||
app.canvas.table_preview_layout.as_ref().map(|(_, _, sel, _)| sel)
|
||||
}
|
||||
|
||||
/// Whether the pages are drawn dark: View › Switch Modes, or in Read Mode the Page color's
|
||||
/// Inverse (Switch Modes is ignored there). Drawing over the pages (ink, drag previews) follows it.
|
||||
pub(crate) fn dark_page(app: &WordApp) -> bool {
|
||||
use wordcraft_engine::cmd::view::ReadPageColor;
|
||||
let v = &app.session.view;
|
||||
if v.read_mode { v.read_page_color == ReadPageColor::Inverse } else { v.dark_mode }
|
||||
}
|
||||
|
||||
/// Render options for this view's page rasters: marks, markup, dark page, header/body dimming.
|
||||
pub(super) fn page_render_options(app: &WordApp, dim_body: bool) -> wordcraft_render::RenderOptions {
|
||||
let mut opts = screen_render_options();
|
||||
@@ -231,14 +239,13 @@ pub(super) fn page_render_options(app: &WordApp, dim_body: bool) -> wordcraft_re
|
||||
// Ink Replay draws the ink itself, stroke by stroke, over pages without it.
|
||||
opts.display.hide_ink = app.session.view.hide_ink || app.session.view.draw.replay;
|
||||
opts.display.revisions = app.session.prefs.markup.clone();
|
||||
opts.dark = app.session.view.dark_mode;
|
||||
opts.dark = dark_page(app);
|
||||
opts.dark_paper = wordcraft_render::DARK_PAPER;
|
||||
// In Read Mode the Page color sets the paper and the dark page, and Switch Modes is ignored.
|
||||
// In Read Mode the Page color sets the paper.
|
||||
if app.session.view.read_mode {
|
||||
use wordcraft_engine::cmd::view::ReadPageColor;
|
||||
let color = app.session.view.read_page_color;
|
||||
opts.read_paper = Some(if color == ReadPageColor::Sepia { wordcraft_render::SEPIA_PAPER } else { wordcraft_doc::props::Rgb::WHITE });
|
||||
opts.dark = color == ReadPageColor::Inverse;
|
||||
}
|
||||
opts.display.dim_header = !dim_body;
|
||||
opts.display.dim_body = dim_body;
|
||||
|
||||
@@ -392,9 +392,12 @@ fn read_mode_page_color_sets_the_reading_paper() {
|
||||
a.run("view.printLayout", json!({})).unwrap();
|
||||
assert_eq!(paper(&a), Rgb(0x10, 0x20, 0x40), "leaving Read Mode brings the page color back");
|
||||
assert!(!page_render_options(&a, false).dark);
|
||||
// With Switch Modes on, Read Mode's paper still follows the Page color: None is white and Sepia is sepia.
|
||||
// With Switch Modes on, Read Mode's paper still follows the Page color, which is kept from
|
||||
// the last entry: Inverse is dark, None is white and Sepia is sepia.
|
||||
a.session.view.dark_mode = true;
|
||||
a.run("view.readMode", json!({})).unwrap();
|
||||
assert!(page_render_options(&a, false).dark, "Inverse is kept");
|
||||
a.run("view.readPageColor", json!({"value": "none"})).unwrap();
|
||||
assert!(!page_render_options(&a, false).dark);
|
||||
assert_eq!(paper(&a), Rgb::WHITE);
|
||||
a.run("view.readPageColor", json!({"value": "sepia"})).unwrap();
|
||||
@@ -403,6 +406,33 @@ fn read_mode_page_color_sets_the_reading_paper() {
|
||||
assert!(page_render_options(&a, false).dark, "Print Layout keeps Switch Modes");
|
||||
}
|
||||
|
||||
/// What the canvas draws over the pages (ink, object drag previews, table drag arrows) and the
|
||||
/// paper drawn under a page before its raster follow Read Mode's Page color, not Switch Modes.
|
||||
#[test]
|
||||
fn drawing_over_the_pages_follows_the_read_mode_page_color() {
|
||||
use wordcraft_doc::props::Rgb;
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("Hello WordCraft")), Default::default());
|
||||
a.run("design.pageColor", json!({"color": "#102040"})).unwrap();
|
||||
let blank = |a: &WordApp| page_render_options(a, false).blank_paper();
|
||||
let grey = Rgb(wordcraft_render::DARK_PAPER, wordcraft_render::DARK_PAPER, wordcraft_render::DARK_PAPER);
|
||||
a.session.view.dark_mode = true;
|
||||
assert!(dark_page(&a), "Switch Modes in Print Layout");
|
||||
assert_eq!(blank(&a), grey);
|
||||
a.run("view.readMode", json!({})).unwrap();
|
||||
assert!(!dark_page(&a), "None in Read Mode, with Switch Modes on");
|
||||
assert_eq!(blank(&a), Rgb::WHITE);
|
||||
a.run("view.readPageColor", json!({"value": "sepia"})).unwrap();
|
||||
assert!(!dark_page(&a));
|
||||
assert_eq!(blank(&a), wordcraft_render::SEPIA_PAPER);
|
||||
a.session.view.dark_mode = false;
|
||||
a.run("view.readPageColor", json!({"value": "inverse"})).unwrap();
|
||||
assert!(dark_page(&a), "Inverse in Read Mode, with Switch Modes off");
|
||||
assert_eq!(blank(&a), grey);
|
||||
a.run("view.printLayout", json!({})).unwrap();
|
||||
assert!(!dark_page(&a));
|
||||
assert_eq!(blank(&a), Rgb::WHITE, "the paper under the raster, not the page color");
|
||||
}
|
||||
|
||||
/// #299: the caret's colour is worked out only when the caret is drawn: with text selected
|
||||
/// there is no caret, so the page's draw list isn't built for it.
|
||||
#[test]
|
||||
|
||||
@@ -232,7 +232,7 @@ pub fn paint(app: &WordApp, painter: &Painter, rects: &[Rect], layout: &DocLayou
|
||||
let s = page_scale(rects, layout, scale, d.page).unwrap_or(scale);
|
||||
let set = app.session.view.draw.settings(d.tool);
|
||||
let alpha = (d.tool.alpha() * 255.0).round() as u8;
|
||||
let color = screen_ink(app.session.view.dark_mode, set.color, alpha);
|
||||
let color = screen_ink(crate::canvas::dark_page(app), set.color, alpha);
|
||||
let pts: Vec<Pos2> = d.pts.iter().map(|p| map(*p)).collect();
|
||||
let width = (set.width * s).max(1.0);
|
||||
if let [only] = pts.as_slice() {
|
||||
@@ -340,7 +340,7 @@ fn paint_replay(app: &WordApp, painter: &Painter, rects: &[Rect], layout: &DocLa
|
||||
let frac = if k.dur > 0.0 { ((r.t - k.start) / k.dur).clamp(0.0, 1.0) } else { 1.0 };
|
||||
let total: f32 = k.paths.iter().map(|p| path_len(p)).sum();
|
||||
let [cr, cg, cb, ca] = k.color.to_srgba_unmultiplied();
|
||||
let color = screen_ink(app.session.view.dark_mode, wordcraft_doc::Rgb(cr, cg, cb), ca);
|
||||
let color = screen_ink(crate::canvas::dark_page(app), wordcraft_doc::Rgb(cr, cg, cb), ca);
|
||||
let mut left = total * frac;
|
||||
let width = (k.width * s).max(1.0);
|
||||
for path in &k.paths {
|
||||
|
||||
@@ -536,7 +536,7 @@ fn preview(app: &WordApp, ctx: &egui::Context, layout: &DocLayout, o: &ObjectHit
|
||||
let obj = app.session.doc.para_at(&o.pos()).and_then(|p| p.object_at(o.off))?;
|
||||
let page = layout.pages.get(o.page)?;
|
||||
let mut opts = crate::canvas::screen_render_options();
|
||||
opts.dark = app.session.view.dark_mode;
|
||||
opts.dark = crate::canvas::dark_page(app);
|
||||
let items = wordcraft_layout::display::object_display(&app.session.doc, page, obj, o.rect, o.text_box, &opts.display);
|
||||
let b = o.bounds();
|
||||
let img = wordcraft_render::render_items(
|
||||
|
||||
@@ -69,15 +69,15 @@ pub(super) fn reading_toolbar(app: &mut WordApp, ui: &mut Ui, t: Tokens) {
|
||||
ui.separator();
|
||||
use wordcraft_engine::cmd::view::ReadPageColor;
|
||||
let colors = [
|
||||
(ReadPageColor::None, "None", "none"),
|
||||
(ReadPageColor::Sepia, "Sepia", "sepia"),
|
||||
(ReadPageColor::Inverse, "Inverse", "inverse"),
|
||||
(ReadPageColor::None, tl!("None"), "none"),
|
||||
(ReadPageColor::Sepia, tl!("Sepia"), "sepia"),
|
||||
(ReadPageColor::Inverse, tl!("Inverse"), "inverse"),
|
||||
];
|
||||
let current = app.session.view.read_page_color;
|
||||
let name = colors.iter().find(|c| c.0 == current).map_or("None", |c| c.1);
|
||||
ui.menu_button(format!("{}: {}", tl!("Page color"), tl!(name)), |ui| {
|
||||
let name = colors.iter().find(|c| c.0 == current).map_or_else(|| tl!("None"), |c| c.1);
|
||||
ui.menu_button(format!("{}: {}", tl!("Page color"), name), |ui| {
|
||||
for (color, name, value) in colors {
|
||||
if ui.selectable_label(current == color, tl!(name)).clicked() {
|
||||
if ui.selectable_label(current == color, name).clicked() {
|
||||
let _ = app.run("view.readPageColor", json!({"value": value}));
|
||||
ui.close();
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ pub fn cursor(app: &WordApp, ui: &Ui, resp: &egui::Response, layout: &DocLayout,
|
||||
/// dragged, a dashed outline of where the table goes.
|
||||
pub fn paint(app: &WordApp, painter: &egui::Painter, t: &Tokens, layout: &DocLayout, rects: &[Rect], scale: f32) {
|
||||
let hover = painter.ctx().input(|i| i.pointer.hover_pos());
|
||||
let dark = app.session.view.dark_mode;
|
||||
let dark = crate::canvas::dark_page(app);
|
||||
if let Some(d) = &app.canvas.table_drag {
|
||||
match d.target.spot {
|
||||
Spot::Handle => {
|
||||
|
||||
@@ -560,6 +560,29 @@ fn alignment_guides_are_a_ui_command_and_survive_a_restart() {
|
||||
assert!(old.alignment_guides, "older settings leave it on");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_page_color_restores_for_new_documents_and_windows() {
|
||||
use wordcraft_engine::cmd::view::ReadPageColor;
|
||||
let mut a = app();
|
||||
a.run("view.readMode", json!({})).unwrap();
|
||||
a.run("view.readPageColor", json!({"value": "sepia"})).unwrap();
|
||||
let saved = serde_json::to_string(&a.prefs()).unwrap();
|
||||
let mut b = app();
|
||||
b.apply_prefs(serde_json::from_str(&saved).unwrap());
|
||||
assert_eq!(b.session.view.read_page_color, ReadPageColor::Sepia);
|
||||
b.run("view.readMode", json!({})).unwrap();
|
||||
assert_eq!(b.session.view.read_page_color, ReadPageColor::Sepia);
|
||||
b.run("file.new", json!({})).unwrap();
|
||||
b.run("view.readMode", json!({})).unwrap();
|
||||
assert_eq!(b.session.view.read_page_color, ReadPageColor::Sepia);
|
||||
let first_window = b.windows.shown();
|
||||
b.new_window();
|
||||
assert_eq!(b.session.view.read_page_color, ReadPageColor::Sepia);
|
||||
b.run("view.readPageColor", json!({"value": "inverse"})).unwrap();
|
||||
assert!(b.enter_window(first_window));
|
||||
assert_eq!(b.session.view.read_page_color, ReadPageColor::Inverse);
|
||||
}
|
||||
|
||||
/// File › Options › Advanced › Animation: the caret's animation is a UI command, kept between
|
||||
/// runs.
|
||||
#[test]
|
||||
@@ -5231,6 +5254,9 @@ fn check_spelling_survives_restart() {
|
||||
// Settings saved before #1792 have no key: spelling stays on.
|
||||
second.apply_prefs(serde_json::from_str("{}").unwrap());
|
||||
assert!(second.session.view.proofing);
|
||||
let conflicting = serde_json::from_value(json!({"checkSpelling": true, "editing": {"checkSpelling": false}})).unwrap();
|
||||
second.apply_prefs(conflicting);
|
||||
assert!(second.session.prefs.check_spelling, "the live spelling setting and its saved copy agree");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+3
-1
@@ -50,7 +50,7 @@ Document title opens Rename Document; the account badge opens Options › Genera
|
||||
| Web Layout width | Text wraps to the whole work area (window width at the current zoom, 144–20,000 pt) and re-wraps once a window resize has settled (not on every frame of a drag) or at once on a zoom, keeping the caret in view; the sheet runs edge to edge with no centred page. Draft and Outline keep their 800 pt column; headless sessions keep 800 pt (#1876) | text across the window | 95% | — |
|
||||
| Scrollbar drag page tip | ✅ while the vertical scrollbar is dragged, a tip beside the pointer names the page at the top of the window in the status bar's words ("Page 7 of 10"), on one line however long the number (#2551, #3341, `canvas::tests::dragging_the_scrollbar_names_the_page_at_the_top`); its look and wording not yet compared with Word's | not compared | 70% | 0.5 |
|
||||
| Web and Draft Layout rendering | Very long pages render only visible full-resolution raster tiles instead of shrinking one huge texture to the GPU size limit; nearby tiles are cached for scrolling | sharp document text | 95% | — |
|
||||
| Reading Mode | Existing Read Mode ribbon button opens a read-only, viewport-column layout with animated horizontal paging, transient width/text/spacing controls, heading navigation (Navigation pane) and resize anchoring; print/export layout unchanged. The arrows, Page Up/Down and Escape work straight after entering it; only whole columns show, centred, at any width, text size or window size; the columns end above the page-turn bar (#1772); Page color: None (white, the document's page color and page picture not shown), Sepia or Inverse (a dark page with light text), as Word's View › Page Color in Read Mode, starting at None on each entry (`view.readPageColor`); Read Mode's View menu: Column Width and Page Color work, Layout is missing ([parity-checklist.md](parity-checklist.md), Read Mode 2/3) | reading-optimized view and navigation | 75% | 1–2 |
|
||||
| Reading Mode | Existing Read Mode ribbon button opens a read-only, viewport-column layout with animated horizontal paging, transient width/text/spacing controls, heading navigation (Navigation pane) and resize anchoring; print/export layout unchanged. The arrows, Page Up/Down and Escape work straight after entering it; only whole columns show, centred, at any width, text size or window size; the columns end above the page-turn bar (#1772); Page color: None (white, the document's page color and page picture not shown), Sepia or Inverse (a dark page with light text), as Word's View › Page Color in Read Mode, starting at None and kept between Read Mode entries, documents and runs (`view.readPageColor`); ink, object drag previews and table drag arrows drawn over the pages follow it; Read Mode's View menu: Column Width and Page Color work, Layout is missing ([parity-checklist.md](parity-checklist.md), Read Mode 2/3) | reading-optimized view and navigation | 75% | 1–2 |
|
||||
| Status bar (page x of y, words, language, view buttons, zoom slider) | ✅ (the page is the caret's while the caret is on screen, else the page at the top of the window, so scrolling updates it (#3880); the language is the caret's, by name; a click sets the proofing language, or opens Set Proofing Language… for any other; "N fonts not installed" when the document's text is drawn in substitutes that measure differently, a click lists them with the faces drawn instead, the first 20 and then how many more, and Add Font…) | ✅ | 85% | — |
|
||||
| Caret motion: keys and typing glide the caret to its new place (100 ms, `cubic-bezier(0, 0, 0, 1)`), the blink fades (530 ms on and off, 100 ms fades); clicks and scripted moves don't glide. File › Options › Advanced › Animation (`ui.caretAnimation`) turns it off. The caret is black or white by contrast with what lies under it (paper, page colour, dark page, shading, highlight), never by the UI theme (#299). On Windows, whose inverting I-beam pointer stays white over the page, the pointer there is drawn the same way: black on light paper, white on dark (#1746) | ✅ as measured from Word for Windows (`caret_glide.rs`); zoom levels other than 100%, modifier keys and off-screen moves not measured | ✅ on Windows; Word for Mac not measured | 90% | 1–2 |
|
||||
| Interface themes (light, dark, system #249, follows OS changes live #311), dark page separate from the interface theme (#194, #312); on a dark page highlights and character shading are dimmed, keeping their hue, so the text on them stays readable; coloured text below 4.5:1 on them is drawn white, and underlines and strikethroughs follow the same rule (#1252); Interface Theme as a searchable View command (light/dark/system, toggle without a value, #1861) | ✅ | ✅ | 90% | — |
|
||||
@@ -97,6 +97,7 @@ Right-click and Shift+F10 keep the compact suggestions menu. The pane is shared
|
||||
| 2026-11-03 | minor | File › Options › Language: Your languages (English variants in use, installed Language packs, the document's languages) with each one's proofing status, Install, Remove and Set as default; Add a language… opens a searchable picker (dutch, nederlands, nl) sorted by name; the Dictionaries folder and the Dictionary files list moved here from Proofing, which has no Dictionaries section any more (CP4.3) |
|
||||
| 2026-10-28 | trivial | Fonts that aren't installed are flagged: the font box (ribbon, mini toolbar, both Font dialog boxes) shows a warning mark whose hover note names the font drawn instead; the status bar says "N fonts not installed" and lists them with the faces drawn instead (the first 20, then how many more), with Add Font…. The list comes from layout (`ParaLayout::substituted`), so it covers exactly the text shown, in every story, list labels and East Asian and complex-script text included, and a missing roman font drawn in Cambria counts. Metric-compatible substitutes (Carlito for Calibri, Caladea for Cambria, Liberation for Arial, Times New Roman and Courier New, the Palatino look-alikes) aren't flagged, since line breaks don't move, nor symbol fonts drawn as their Unicode equivalents (Word's bullets); headers and footers count only in views that show them, chart and SmartArt labels not yet; the font box reads the same list, so for text already shown the two agree, and it also warns for a missing font no text shows yet when the box shows a font to come: at a collapsed caret, the font the next letter will be typed in, and in the Font dialog, the fonts OK applies (the Asian text font too, over Latin text); Missing Fonts (`font.missing`, from command search or agents; it says what it found in the status line) and `format.state.fontShownAs` for agents |
|
||||
| 2026-10-31 | trivial | Keytips: Alt, F badges the Backstage pages with Word's letters and the letter opens the page; Alt, F, P and Alt, F, V open Print; the badges end with the Backstage (#4960) |
|
||||
| 2026-10-30 | trivial | Read Mode keeps the selected Page color between entries, documents and runs. The color does not change document formatting. |
|
||||
| 2026-10-31 | trivial | View › Macros opens a Macros dialog (also Alt+F8 and command search): the macros of this session listed with Run, Delete and Record, which reads Stop Recording while a recording runs; the button did nothing before, as `tools.macros` without a macro to run, define or delete only returned the list (#4327) |
|
||||
| 2026-10-31 | trivial | Synchronize the existing Proofing pane with main `eecbb8f`; preserve explicit-position lookup, implicit selection, navigation, undo and per-window state; fresh exact-SHA hosted validation pending, readiness unchanged |
|
||||
| 2026-10-30 | trivial | Synchronize the existing Proofing pane and navigation/selection/undo regressions with main `3b7f7ed`; new-SHA validation pending, readiness unchanged |
|
||||
@@ -114,6 +115,7 @@ Right-click and Shift+F10 keep the compact suggestions menu. The pane is shared
|
||||
| 2026-10-22 | trivial | View › Window › New Window opens another window on the document, focused and cascaded; two windows on one document are titled `Name:1` and `Name:2` (one window: the plain name). Each window keeps its own selection, scroll position, zoom, view mode, ruler and panes; the text, undo history, unsaved state and file are the document's, so an edit or Undo in either window shows in both, and a caret in text the other window deleted moves to the nearest place left. Closing one of a document's windows asks nothing; closing its last asks about unsaved changes, and quitting asks once per document. Switch Windows lists every window. The web app keeps one window |
|
||||
| 2026-10-22 | trivial | One native window per open document on the desktop (the web app keeps one window): New and Open open the document in a window of its own, focused and cascaded from the last; Open replaces an untouched blank document in its window and brings an already-open file's window forward; each window keeps its own ribbon tab, File page, dialogs, panes and composition; View › Window › Switch Windows lists the windows and brings the chosen one forward; File › Close and the window's close button close the window (asking about unsaved changes) and the window focused before comes forward, while the last window stays open with no document; quitting asks Save / Don't Save / Cancel about each document in its own window |
|
||||
| 2026-10-22 | trivial | Several open documents in one window (one window per document comes next): View › Window › Switch Windows lists the open documents, the shown one marked and unsaved ones dotted, and shows the one chosen, with its own scroll position, zoom, comment drafts and AutoSave file; File › New, Open, Envelopes, Labels, Finish & Merge and personal templates open beside the open documents without asking about unsaved changes; File › Close shows the next open document; the window's close button asks Save / Don't Save / Cancel about each document with unsaved changes, showing it, and Cancel keeps the app open. Untitled documents are numbered (Document2) in the interface language |
|
||||
| 2026-10-25 | trivial | Read Mode: the Page color is kept from one entry to the next; ink, object drag previews and table drag arrows follow it instead of Switch Modes |
|
||||
| 2026-10-25 | trivial | Custom Document Properties dialog: File › Info › Properties › Advanced Properties… opens it; the document's custom properties listed with Add, Modify and Delete over a name/value form (text properties only; Word's number/date/yes-no types not offered), OK one undoable `file.properties` step, Cancel discards; modal dialogs 62 → 63 (#2982) |
|
||||
| 2026-10-25 | trivial | Text Highlight Color shows the remembered colour on the ribbon and mini-toolbar buttons; No Color leaves the pen without a misleading yellow bar (#4087) |
|
||||
| 2026-10-24 | trivial | Read Mode: a Page color menu (None, Sepia, Inverse) beside Line spacing; the columns are drawn on white, sepia or a dark page, in place of the document's page color (`view.readPageColor`) |
|
||||
|
||||
Reference in New Issue
Block a user