mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 19:37:03 -06:00
* #1002: equation pictures share one texture, keeping theme colours on Windows The Equation tab's gallery tiles, structure buttons and symbols were a texture each: hundreds of small textures, created as galleries opened and all freed at once when the preview cache filled. On Windows (DirectX 12) tiles then showed other textures' memory (inverted pictures, red and teal fills, damaged interface letters). They are now packed into one atlas texture written in place, the way egui keeps its glyphs; it starts over when full (at most once a frame). Tests: every equation picture composited in both themes is only the theme's greys; all pictures share one texture, none freed while browsing, none overlapping; the atlas packs rows and starts over when full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1002: the theme test looks for paper anywhere, not in the corner (Windows fonts reach it) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1002: equation atlas uses up to four pages and never wipes a page drawn from this frame Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Dmitrii Khlapov
Brandon Thomas
parent
aa113f0801
commit
ff15dc451d
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: progress log: equations copy as MathML, #816; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
|
||||
|
||||
@@ -147,6 +147,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-13 | Pagination with footnotes follows Word (#1116): a paragraph ending on a page with footnotes keeps its space after above the separator, otherwise its last lines move to the next page with the note; pages without notes are unchanged |
|
||||
| 2026-10-13 | Interface size means what it says (#822): a chosen size is absolute, as in PhotoCraft and PdfCraft, so "100%" is one screen pixel per point even where the system reports 200 % (a bare window manager on X11, where winit guesses the scale from the monitor's DPI). The new default, Auto, keeps the system's display scale and its menu entry shows the size it draws at; sizes run 75–300 %. A saved size from before stays the same if it was the default (now Auto) and becomes absolute otherwise |
|
||||
| 2026-10-13 | Copying an equation also puts MathML in HTML on the clipboard beside the linear text like `y=2x_s` (#816; Word/LibreOffice paste not yet verified; OMML is #1297). HTML export writes equations as MathML too, and opening or pasting HTML with MathML gives equations back |
|
||||
| 2026-10-13 | The Equation tab's galleries, structure buttons and symbols keep the theme's colours on Windows: their pictures share a few textures written in place (no wipe of pictures already on screen, room for the built-ins at high display scales), instead of hundreds of small textures freed in bulk, which on DirectX 12 showed inverted, red or teal tiles (#1002) |
|
||||
| 2026-10-12 | RTF import now uses the RTF spec's defaults where a file names none (#902): no space before or after paragraphs, single line spacing, and the `\deff` default font, instead of WordCraft's Normal style; RTF files that relied on the old defaults open tighter, like in Word |
|
||||
| 2026-10-11 | Right-clicking a table cell offers the table's commands, and as in Word they follow the selection: with the caret in a cell Insert, Delete Cells, Split Cells, Text Direction and Table Properties…; with several cells Merge Cells instead of Split; with whole columns or rows Delete Columns or Rows, Merge Cells and Distribute Rows and Columns; with the whole table Delete Table, Distribute, AutoFit and Insert Caption…. A right-click beside selected cells moves the caret there instead of keeping them |
|
||||
| 2026-10-11 | Drags, Shift+arrows and Shift+click over table cells select whole cells, as in Word: from one cell into another the rectangle between them (bold, alignment and Delete leave the other cells alone), into or out of a table whole rows; extending again goes on from where the selection started |
|
||||
@@ -294,6 +295,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-13 | trivial | Progress log: clicks on ribbon chrome keep typing in the document (#1093) |
|
||||
| 2026-10-13 | trivial | Progress log: dragged shapes wrap in their new column (#884) |
|
||||
| 2026-10-12 | trivial | Progress log: Accept All and Reject All keep the right paragraph marks (#864) |
|
||||
| 2026-10-13 | trivial | Progress log: equation pictures keep the theme colours on Windows (#1002) |
|
||||
| 2026-10-11 | trivial | Progress log: Ctrl, Alt, left-edge and selected-cells table border drags (#711) |
|
||||
| 2026-10-11 | trivial | Progress log: table markers on the rulers (#700) |
|
||||
| 2026-10-11 | trivial | Progress log: the table context menu |
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::theme::{Tokens, regular, semibold};
|
||||
use crate::widgets::{BIG_ICON_Y, BIG_LABEL_Y, CONTENT_H, LABEL_PITCH, group, small};
|
||||
|
||||
/// What each structure button shows.
|
||||
fn button_picture(id: &str) -> &'static str {
|
||||
pub(crate) fn button_picture(id: &str) -> &'static str {
|
||||
match id {
|
||||
"fraction" => "x/y",
|
||||
"script" => "e^x",
|
||||
@@ -35,8 +35,8 @@ fn button_picture(id: &str) -> &'static str {
|
||||
fn paint_math(app: &mut WordApp, ui: &Ui, r: Rect, key: &str, linear: &str, nodes: Option<wordcraft_doc::math::Arg>, display: bool, pt: f32) {
|
||||
let nodes = nodes.unwrap_or_else(|| parse_linear(linear));
|
||||
match crate::previews::math_texture(app, ui.ctx(), key, &nodes, display, pt, r.size()) {
|
||||
Some(tex) => {
|
||||
ui.painter().image(tex.id(), r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Tokens::get(ui.ctx()).text);
|
||||
Some((tex, uv)) => {
|
||||
ui.painter().image(tex, r, uv, Tokens::get(ui.ctx()).text);
|
||||
}
|
||||
None => {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
|
||||
@@ -26,6 +26,129 @@ pub struct Previews {
|
||||
/// Previews rendered this frame (rendering is spread over frames).
|
||||
budget: std::cell::Cell<u32>,
|
||||
frame: u64,
|
||||
/// The Equation tab's pictures, packed into one texture (#1002).
|
||||
math: MathAtlas,
|
||||
}
|
||||
|
||||
/// Side of each square texture page the equation pictures share, in pixels.
|
||||
const MATH_ATLAS: usize = 2048;
|
||||
/// At most this many pages: enough for a frame's pictures at a high display scale (the 22
|
||||
/// built-in equations alone take two pages at 3×), still a handful of textures in all.
|
||||
const MATH_ATLAS_PAGES: usize = 4;
|
||||
/// Transparent pixels between pictures, so linear filtering never bleeds one into the next.
|
||||
const MATH_ATLAS_GAP: usize = 2;
|
||||
|
||||
/// Every equation picture (gallery tiles, structure buttons, symbols) packed into a few shared
|
||||
/// textures ("pages"), row by row, and drawn as a part of one (#1002).
|
||||
///
|
||||
/// They used to be a texture each: hundreds of small textures, created as galleries opened and
|
||||
/// all freed at once when the preview cache filled. On Windows (DirectX 12) the tiles then
|
||||
/// showed other textures' memory: inverted pictures, red and teal fills, and even damaged
|
||||
/// interface letters. A texture written in place is what egui does for its own glyphs; the
|
||||
/// atlas creates at most [`MATH_ATLAS_PAGES`] textures and frees none while galleries are
|
||||
/// browsed. When every page is full, the page least recently drawn from starts over, but never
|
||||
/// one drawn from in the current frame (its pictures are already on screen): a frame whose
|
||||
/// pictures don't fit shows the rest as linear text.
|
||||
#[derive(Default)]
|
||||
struct MathAtlas {
|
||||
pages: Vec<AtlasPage>,
|
||||
/// Picture key → its page and part of that page's texture (UV coordinates).
|
||||
uv: HashMap<String, (usize, Rect)>,
|
||||
/// How many times a page started over (for tests).
|
||||
wipes: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AtlasPage {
|
||||
tex: Option<TextureHandle>,
|
||||
/// Where the next picture goes: the current row's left edge and top, and the row's height.
|
||||
x: usize,
|
||||
y: usize,
|
||||
row_h: usize,
|
||||
/// The last frame a picture of this page was handed out in.
|
||||
used: u64,
|
||||
}
|
||||
|
||||
impl AtlasPage {
|
||||
/// A free spot for a `w`×`h` picture, or `None` when the page is full or it can't fit.
|
||||
fn place(&mut self, w: usize, h: usize) -> Option<[usize; 2]> {
|
||||
if w == 0 || h == 0 || w > MATH_ATLAS || h > MATH_ATLAS {
|
||||
return None;
|
||||
}
|
||||
let (mut x, mut y, mut row_h) = (self.x, self.y, self.row_h);
|
||||
if x.saturating_add(w) > MATH_ATLAS {
|
||||
x = 0;
|
||||
y = y.saturating_add(row_h).saturating_add(MATH_ATLAS_GAP);
|
||||
row_h = 0;
|
||||
}
|
||||
if y.saturating_add(h) > MATH_ATLAS {
|
||||
return None;
|
||||
}
|
||||
self.x = x.saturating_add(w).saturating_add(MATH_ATLAS_GAP);
|
||||
self.y = y;
|
||||
self.row_h = row_h.max(h);
|
||||
Some([x, y])
|
||||
}
|
||||
|
||||
/// Forget the page's layout and clear its texture.
|
||||
fn reset(&mut self) {
|
||||
self.x = 0;
|
||||
self.y = 0;
|
||||
self.row_h = 0;
|
||||
if let Some(tex) = &mut self.tex {
|
||||
tex.set(egui::ColorImage::filled([MATH_ATLAS, MATH_ATLAS], egui::Color32::TRANSPARENT), egui::TextureOptions::LINEAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MathAtlas {
|
||||
/// The picture stored under `key`, marking its page as drawn from in `frame`.
|
||||
fn get(&mut self, frame: u64, key: &str) -> Option<(egui::TextureId, Rect)> {
|
||||
let (page, uv) = *self.uv.get(key)?;
|
||||
let p = self.pages.get_mut(page)?;
|
||||
let id = p.tex.as_ref()?.id();
|
||||
p.used = frame;
|
||||
Some((id, uv))
|
||||
}
|
||||
|
||||
/// Add `img` under `key`; its texture and UV rectangle, or `None` if it can't be placed in
|
||||
/// this frame without wiping pictures already handed out in it.
|
||||
fn add(&mut self, ctx: &egui::Context, frame: u64, key: &str, img: egui::ColorImage) -> Option<(egui::TextureId, Rect)> {
|
||||
let [w, h] = img.size;
|
||||
if w == 0 || h == 0 || w > MATH_ATLAS || h > MATH_ATLAS {
|
||||
return None;
|
||||
}
|
||||
let mut spot = self.pages.iter_mut().enumerate().find_map(|(i, p)| p.place(w, h).map(|at| (i, at)));
|
||||
if spot.is_none() && self.pages.len() < MATH_ATLAS_PAGES {
|
||||
let mut p = AtlasPage::default();
|
||||
spot = p.place(w, h).map(|at| (self.pages.len(), at));
|
||||
self.pages.push(p);
|
||||
}
|
||||
if spot.is_none() {
|
||||
// Every page is full: start over the one drawn from longest ago, unless all of them
|
||||
// were drawn from in this frame.
|
||||
let (i, p) = self.pages.iter_mut().enumerate().filter(|(_, p)| p.used != frame).min_by_key(|(_, p)| p.used)?;
|
||||
p.reset();
|
||||
self.uv.retain(|_, (page, _)| *page != i);
|
||||
self.wipes = self.wipes.saturating_add(1);
|
||||
spot = p.place(w, h).map(|at| (i, at));
|
||||
}
|
||||
let (i, at) = spot?;
|
||||
let p = self.pages.get_mut(i)?;
|
||||
let tex = p.tex.get_or_insert_with(|| {
|
||||
ctx.load_texture(
|
||||
format!("math_atlas_{i}"),
|
||||
egui::ColorImage::filled([MATH_ATLAS, MATH_ATLAS], egui::Color32::TRANSPARENT),
|
||||
egui::TextureOptions::LINEAR,
|
||||
)
|
||||
});
|
||||
tex.set_partial(at, img, egui::TextureOptions::LINEAR);
|
||||
p.used = frame;
|
||||
let s = MATH_ATLAS as f32;
|
||||
let uv = Rect::from_min_max(pos2(at[0] as f32 / s, at[1] as f32 / s), pos2((at[0] + w) as f32 / s, (at[1] + h) as f32 / s));
|
||||
self.uv.insert(key.to_string(), (i, uv));
|
||||
Some((tex.id(), uv))
|
||||
}
|
||||
}
|
||||
|
||||
impl Previews {
|
||||
@@ -182,8 +305,9 @@ impl Previews {
|
||||
}
|
||||
|
||||
/// An equation rasterised for a gallery tile or a button (`pt`: font size; `size`: the tile in
|
||||
/// points), with its empty slots shown. Cached by `key`; `None` while waiting for this frame's
|
||||
/// rendering budget.
|
||||
/// points), with its empty slots shown: the shared texture and the picture's UV rectangle in it
|
||||
/// ([`MathAtlas`]). An ink mask, drawn tinted with the theme's text colour. `None` while waiting
|
||||
/// for this frame's rendering budget.
|
||||
pub fn math_texture(
|
||||
app: &mut WordApp,
|
||||
ctx: &egui::Context,
|
||||
@@ -192,10 +316,25 @@ pub fn math_texture(
|
||||
display: bool,
|
||||
pt: f32,
|
||||
size: egui::Vec2,
|
||||
) -> Option<TextureHandle> {
|
||||
) -> Option<(egui::TextureId, Rect)> {
|
||||
let ppp = ctx.pixels_per_point();
|
||||
let k = format!("math:{key}:{pt}:{}x{}:{ppp}", size.x, size.y);
|
||||
app.previews.get_or(ctx, &k, || math_image(nodes, display, pt, size.x, size.y, ppp))
|
||||
let p = &mut app.previews;
|
||||
let frame = ctx.cumulative_frame_nr();
|
||||
if let Some(hit) = p.math.get(frame, &k) {
|
||||
return Some(hit);
|
||||
}
|
||||
if frame != p.frame {
|
||||
p.frame = frame;
|
||||
p.budget.set(0);
|
||||
}
|
||||
if p.budget.get() > 8 {
|
||||
ctx.request_repaint();
|
||||
return None;
|
||||
}
|
||||
p.budget.set(p.budget.get() + 1);
|
||||
let img = math_image(nodes, display, pt, size.x, size.y, ppp)?;
|
||||
p.math.add(ctx, frame, &k, img)
|
||||
}
|
||||
|
||||
fn math_image(nodes: &wordcraft_doc::math::Arg, display: bool, pt: f32, w: f32, h: f32, ppp: f32) -> Option<egui::ColorImage> {
|
||||
@@ -603,6 +742,183 @@ mod tests {
|
||||
assert!(img.pixels.iter().all(|p| *p == egui::Color32::from_white_alpha(p.a())), "ink is white, to be tinted");
|
||||
}
|
||||
|
||||
/// Every picture the Equation tab and Insert › Equation draw: (key, nodes, display, pt, tile size).
|
||||
fn equation_pictures() -> Vec<(String, wordcraft_doc::math::Arg, bool, f32, egui::Vec2)> {
|
||||
use wordcraft_doc::math::{MNode, MRun, parse_linear};
|
||||
use wordcraft_engine::math_gallery::{BUILT_INS, STRUCTURES, SYMBOL_SETS};
|
||||
let mut out = Vec::new();
|
||||
for g in STRUCTURES {
|
||||
let pic = crate::equation_tab::button_picture(g.id);
|
||||
out.push((format!("btn:{pic}"), parse_linear(pic), false, 12.0, vec2(40.0, 32.0)));
|
||||
for (_, templates) in g.sections {
|
||||
for t in templates.iter() {
|
||||
out.push((format!("tpl:{}", t.id), t.nodes(), true, 14.0, vec2(56.0, 52.0)));
|
||||
out.push((format!("tpl:{}", t.id), t.nodes(), true, 13.0, vec2(154.0, 56.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (id, _, lin) in BUILT_INS {
|
||||
out.push((format!("builtin:{id}"), parse_linear(lin), true, 11.0, vec2(346.0, 60.0)));
|
||||
}
|
||||
for (_, chars) in SYMBOL_SETS {
|
||||
for c in chars.chars() {
|
||||
let nodes = vec![MNode::Run(MRun { text: c.to_string(), lit: true, ..Default::default() })];
|
||||
out.push((format!("sym:{c}"), nodes, false, 11.0, vec2(22.0, 22.0)));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// #1002: drawn the way the Equation tab draws them (the picture tinted with the theme's text
|
||||
/// colour on an input-coloured tile), every gallery tile, button and symbol is the theme's
|
||||
/// greys only: its background is the tile colour and its ink no darker or lighter than the
|
||||
/// text, in light and dark mode, with no red or teal anywhere.
|
||||
#[test]
|
||||
fn equation_pictures_follow_the_theme() {
|
||||
let mix = |fg: u8, bg: u8, a: u8| ((u32::from(fg) * u32::from(a) + u32::from(bg) * (255 - u32::from(a))) / 255) as u8;
|
||||
for (key, nodes, display, pt, size) in equation_pictures() {
|
||||
let img = math_image(&nodes, display, pt, size.x, size.y, 1.0).unwrap_or_else(|| panic!("{key} renders"));
|
||||
assert!(img.pixels.iter().all(|p| *p == egui::Color32::from_white_alpha(p.a())), "{key}: an ink mask");
|
||||
// Ink may reach the corner (a matrix bracket with Windows fonts): look for paper anywhere.
|
||||
assert!(img.pixels.iter().any(|p| p.a() == 0), "{key}: transparent paper");
|
||||
for t in [Tokens::light(), Tokens::dark()] {
|
||||
let (bg, fg) = (t.input, t.text);
|
||||
let (lo, hi) = (bg.r().min(fg.r()), bg.r().max(fg.r()));
|
||||
for p in &img.pixels {
|
||||
let a = p.a();
|
||||
let c = [mix(fg.r(), bg.r(), a), mix(fg.g(), bg.g(), a), mix(fg.b(), bg.b(), a)];
|
||||
assert!(c[0] == c[1] && c[1] == c[2], "{key}: a grey, not {c:?}");
|
||||
assert!((lo..=hi).contains(&c[0]), "{key}: between the tile and the text colour, not {c:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #1002: the pictures share one texture, written in place: browsing every gallery creates no
|
||||
/// texture per tile and frees none (hundreds of small textures, freed in bulk, showed other
|
||||
/// textures' memory on DirectX 12).
|
||||
#[test]
|
||||
fn equation_pictures_share_one_texture() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = WordApp::new(wordcraft_engine::Session::new(Document::new()), crate::Services::default());
|
||||
let pictures = equation_pictures();
|
||||
// Symbols repeat across sets (∀, ≠…): each is drawn once.
|
||||
let unique = pictures.iter().map(|(key, _, _, pt, size)| format!("{key}:{pt}:{size:?}")).collect::<std::collections::HashSet<_>>().len();
|
||||
let mut drawn: HashMap<String, (egui::TextureId, Rect)> = HashMap::new();
|
||||
let mut freed = 0;
|
||||
for _ in 0..200 {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let ctx = ui.ctx().clone();
|
||||
for (key, nodes, display, pt, size) in &pictures {
|
||||
let k = format!("{key}:{pt}:{size:?}");
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = drawn.entry(k)
|
||||
&& let Some(t) = math_texture(&mut app, &ctx, key, nodes, *display, *pt, *size)
|
||||
{
|
||||
e.insert(t);
|
||||
}
|
||||
}
|
||||
});
|
||||
freed += out.textures_delta.free.len();
|
||||
out.textures_delta.clear();
|
||||
if drawn.len() == unique {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(drawn.len(), unique, "every picture is drawn");
|
||||
let ids: std::collections::HashSet<_> = drawn.values().map(|(id, _)| *id).collect();
|
||||
assert_eq!(ids.len(), 1, "one texture for every picture");
|
||||
assert_eq!(freed, 0, "no texture freed while browsing");
|
||||
// No two pictures overlap, and each lies inside the texture.
|
||||
let rects: Vec<Rect> = drawn.values().map(|(_, uv)| *uv).collect();
|
||||
for (i, a) in rects.iter().enumerate() {
|
||||
assert!(a.min.x >= 0.0 && a.min.y >= 0.0 && a.max.x <= 1.0 && a.max.y <= 1.0, "{a:?} inside");
|
||||
for b in rects.iter().skip(i + 1) {
|
||||
assert!(!a.intersects(*b) || a.intersect(*b).area() <= 0.0, "{a:?} overlaps {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #1002: pictures are packed row by row, and pictures that can never fit are refused.
|
||||
#[test]
|
||||
fn math_atlas_packs_rows() {
|
||||
let mut a = AtlasPage::default();
|
||||
assert_eq!(a.place(100, 40), Some([0, 0]));
|
||||
assert_eq!(a.place(100, 20), Some([100 + MATH_ATLAS_GAP, 0]));
|
||||
assert_eq!(a.place(MATH_ATLAS, 10), Some([0, 40 + MATH_ATLAS_GAP]), "a new row under the tallest");
|
||||
assert_eq!(a.place(MATH_ATLAS + 1, 10), None);
|
||||
assert_eq!(a.place(10, 0), None);
|
||||
assert_eq!(a.place(10, MATH_ATLAS), None, "full");
|
||||
assert_eq!(a.place(10, 10), Some([0, 40 + MATH_ATLAS_GAP + 10 + MATH_ATLAS_GAP]), "still room after a refusal");
|
||||
}
|
||||
|
||||
/// #1002: a page is never wiped in a frame that handed out one of its pictures (they are
|
||||
/// already on screen); once all pages are full, the least recently drawn page starts over.
|
||||
#[test]
|
||||
fn math_atlas_never_wipes_a_page_drawn_from_this_frame() {
|
||||
let ctx = egui::Context::default();
|
||||
let img = || egui::ColorImage::filled([MATH_ATLAS, MATH_ATLAS], egui::Color32::TRANSPARENT);
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let mut a = MathAtlas::default();
|
||||
let first: Vec<_> = (0..MATH_ATLAS_PAGES).map(|i| a.add(ui.ctx(), 1, &format!("p{i}"), img())).collect();
|
||||
assert!(first.iter().all(Option::is_some), "a page each");
|
||||
let ids: std::collections::HashSet<_> = first.iter().flatten().map(|(id, _)| *id).collect();
|
||||
assert_eq!(ids.len(), MATH_ATLAS_PAGES);
|
||||
assert!(a.add(ui.ctx(), 1, "more", img()).is_none(), "every page was drawn from this frame");
|
||||
assert_eq!(a.wipes, 0);
|
||||
assert!(a.uv.len() == MATH_ATLAS_PAGES, "nothing forgotten");
|
||||
// Next frame: page 0 is drawn from again, so the one drawn longest ago is page 1.
|
||||
assert_eq!(a.get(2, "p0"), first.first().copied().flatten());
|
||||
let (id, _) = a.add(ui.ctx(), 2, "more", img()).expect("a page starts over");
|
||||
assert_eq!(a.wipes, 1);
|
||||
assert_eq!(Some(id), first.get(1).copied().flatten().map(|(id, _)| id), "the least recently drawn page");
|
||||
assert!(a.uv.contains_key("p0") && !a.uv.contains_key("p1"));
|
||||
// Pages 0 and 1 are now in use this frame; 2 and 3 are free to start over, then none.
|
||||
assert!(a.add(ui.ctx(), 2, "x", img()).is_some());
|
||||
assert!(a.add(ui.ctx(), 2, "y", img()).is_some());
|
||||
assert!(a.add(ui.ctx(), 2, "z", img()).is_none());
|
||||
assert_eq!(a.wipes, 3);
|
||||
assert!(a.get(2, "p0").is_some() && a.get(2, "more").is_some() && a.get(2, "x").is_some() && a.get(2, "y").is_some());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
/// #1002: at a display scale of 3, the 22 built-in equations need more than one page; the
|
||||
/// built-in menu opened again draws them all from the atlas, with nothing wiped or moved
|
||||
/// (a single page used to start over every frame: flicker and constant re-rendering).
|
||||
#[test]
|
||||
fn builtin_menu_at_scale_3_fits_without_wiping() {
|
||||
use wordcraft_doc::math::parse_linear;
|
||||
use wordcraft_engine::math_gallery::BUILT_INS;
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_pixels_per_point(3.0);
|
||||
let mut app = WordApp::new(wordcraft_engine::Session::new(Document::new()), crate::Services::default());
|
||||
let builtins: Vec<_> = BUILT_INS.iter().map(|(id, _, lin)| (format!("builtin:{id}"), parse_linear(lin))).collect();
|
||||
let draw = |app: &mut WordApp| {
|
||||
let mut got = Vec::new();
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
assert_eq!(ui.ctx().pixels_per_point(), 3.0);
|
||||
let ctx = ui.ctx().clone();
|
||||
got = builtins.iter().map(|(key, nodes)| math_texture(app, &ctx, key, nodes, true, 11.0, vec2(346.0, 60.0))).collect();
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
got
|
||||
};
|
||||
// The first opening renders a few a frame.
|
||||
let mut first = draw(&mut app);
|
||||
for _ in 0..20 {
|
||||
if first.iter().all(Option::is_some) {
|
||||
break;
|
||||
}
|
||||
first = draw(&mut app);
|
||||
}
|
||||
assert!(first.iter().all(Option::is_some), "every built-in drawn");
|
||||
let wipes = app.previews.math.wipes;
|
||||
let again = draw(&mut app);
|
||||
assert_eq!(again, first, "the same texture and place for each");
|
||||
assert_eq!(app.previews.math.wipes, wipes, "nothing started over");
|
||||
assert_eq!(wipes, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_mask_keeps_the_ink_and_drops_the_paper() {
|
||||
let mut img = egui::ColorImage::new([3, 1], vec![egui::Color32::WHITE, egui::Color32::BLACK, egui::Color32::from_gray(128)]);
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# UI parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (no document behind the Start screen, #1129; previously trivial: copied equations reach other programs as MathML, #816; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
|
||||
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
|
||||
@@ -102,6 +102,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
|
||||
| 2026-10-11 | trivial | Menu buttons (Header, Page Number, Breaks, Wrap Text, table Select/Delete/AutoFit…) grey out and don't open while the command their menu is about is disabled, like the other ribbon buttons (#658) |
|
||||
| 2026-10-11 | trivial | One drop-down marker everywhere: large ribbon buttons, Equation and the Editing/Reviewing button draw the `dropdown` icon after their label, like split and menu buttons, instead of a text glyph (#659) |
|
||||
| 2026-10-11 | trivial | Dark theme: Styles, style-set and table-style previews sit on a dimmer page tone; with View › Switch Modes on, the Styles gallery previews the dark page (#663) |
|
||||
| 2026-10-13 | trivial | Equation tab and Insert › Equation: every gallery tile, structure button and symbol is drawn from a few shared textures written in place (at most four, never wiped while their pictures are on screen), so the pictures stay in the theme's tile and text colours on Windows (DirectX 12) instead of turning inverted, red or teal (#1002) |
|
||||
| 2026-10-11 | trivial | Dialog primary buttons, the Discord pill, the table eraser line and the Navigation search highlight take their colours from the theme in both themes (`widgets::primary_button`); a test bans the old fixed values (#656) |
|
||||
| 2026-10-11 | trivial | Dark theme: Share, the avatar initials, the AutoSave knob and keytips use dark `on_accent` text on the light accent (6.2:1, 8.4:1 on hover; white was 2.9:1) (#655) |
|
||||
| 2026-10-11 | trivial | Text field and combo outlines reach 3:1 against their fill and surroundings in both themes (light #8A8A8A, dark #7A7A7A); a test pins the ratios (#657) |
|
||||
|
||||
Reference in New Issue
Block a user