Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/proof-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Proof UI

on:
push:
branches: [main]
paths: ["crates/proof-ui/**", "Cargo.toml", "Cargo.lock", ".github/workflows/proof-ui.yml"]
pull_request:
paths: ["crates/proof-ui/**", "Cargo.toml", "Cargo.lock", ".github/workflows/proof-ui.yml"]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: proof-ui-${{ github.ref }}
cancel-in-progress: true

jobs:
linux:
timeout-minutes: 15
name: Proof UI (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
key: craft-proof-ui-linux
- run: cargo fmt -p craft-proof-ui --check
- run: cargo clippy -p craft-proof-ui --all-targets --locked -- -D warnings
- run: cargo test -p craft-proof-ui --locked
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ craft-pcraft-format = { path = "crates/pcraft-format" }
craft-prepress = { path = "crates/prepress" }
craft-print = { path = "crates/print" }
craft-proof = { path = "crates/proof" }
craft-proof-ui = { path = "crates/proof-ui" }
craft-quit = { path = "crates/quit" }
craft-settings = { path = "crates/settings" }
craft-text = { path = "crates/text" }
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
craft-i18n's `language-data/`, embedded at build time from `CRAFT_I18N_DIR`: English and the
carry-over locales by default, the set `CRAFT_PROOF_EMBED` names otherwise (`en` for English
only); other languages come from installed Language packs.
- [`craft-proof-ui`](crates/proof-ui/README.md): the spelling review: a panel that steps through a
document's issues, and a section for the right-click menu.

- [`craft-quit`](crates/quit/src/lib.rs): native macOS quit, egui close handling and an optional
app menu. Unsaved-document decisions and save prompts stay in each app.
Expand Down
18 changes: 18 additions & 0 deletions crates/proof-ui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "craft-proof-ui"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
description = "The spelling review for the Craft apps: a panel that steps through a document's issues, and a right-click section"

[lints]
workspace = true

[dependencies]
egui = { workspace = true }

[dev-dependencies]
egui = { workspace = true, features = ["default_fonts"] }
egui_kittest = "0.37"
34 changes: 34 additions & 0 deletions crates/proof-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Craft Proof UI

`craft-proof-ui` is the spelling review for the Craft apps: a panel that steps through a document's
issues beside the page, and a section for the right-click menu on a misspelled word. The application
supplies its documents through `Host`; findings usually come from `craft-proof`, which this crate
does not depend on.

## Boundaries

- The review and its interface only. The app owns proofing, its text model, applying corrections and
undo, where the panel docks, and how it opens.
- Dependencies: `egui`.
- Never crashes: no panics, no `unsafe`. A failed host call becomes a message in the panel.
- The document stays editable during a review, so nothing is trusted to still be true: a correction
is checked against a fresh `Host::check`, and text that changed asks for a new pass.
- Suggestions are requested for the current issue only, never for the rest of a book's findings.

## Use

```rust,ignore
use craft_proof_ui::{Colors, Host, Review, context_section, panel};

// Keep a `Review` while the panel is open. `start` checks the document from the top.
let mut review = Review::default();
review.start(&mut host);
// Each frame, inside the app's dock. It fills the height it's given and scrolls its own content:
panel(ui, &mut review, &mut host, &text, &colors);
// While the panel shows, the app can underline what's left in the document:
for issue in review.marks(document, |story| version(story)) { /* draw it */ }
// On a right-click the app finds the issue under the caret, once. Then, in its menu:
if let Some(chosen) = context_section(ui, &issue, &text) { /* act on it, close the menu */ }
```

`STRINGS` lists every string looked up through `Text::ui`, for the app's translation catalog.
64 changes: 64 additions & 0 deletions crates/proof-ui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! The spelling review for the Craft apps: see the README.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]

mod review;
mod ui;

pub use review::{Action, Review};
pub use ui::{Colors, ContextChoice, STRINGS, Text, context_section, panel};

/// One finding. `story` is the host's own id for a run of text; `start..end` are UTF-8 byte
/// offsets into it.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Issue {
pub story: u64,
pub start: usize,
pub end: usize,
pub word: String,
pub kind: IssueKind,
/// The explanation to show, in the language of the text; may be empty.
pub message: String,
/// Empty until [`Host::suggestions`] is asked for this issue.
pub suggestions: Vec<String>,
}

impl Issue {
fn same_finding(&self, other: &Issue) -> bool {
(self.story, self.start, self.end, self.kind) == (other.story, other.start, other.end, other.kind) && self.word == other.word
}
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IssueKind {
#[default]
Spelling,
/// A preferred form or a simple rule: the word is known, so it can't join the dictionary.
Grammar,
}

/// What the review needs from the application, for its open document.
pub trait Host {
/// Every issue in the document in reading order. Suggestions may be left empty.
fn check(&mut self) -> Result<Vec<Issue>, String>;
/// The candidates for one issue, best first. An error when it no longer applies.
fn suggestions(&mut self, issue: &Issue) -> Result<Vec<String>, String>;
/// The current text of a story, `None` when it is gone.
fn text(&self, story: u64) -> Option<&str>;
/// A token that changes whenever a story's text might have, cheap enough for every frame.
fn version(&self, story: u64) -> Option<u64>;
/// Identifies the document: the review ends when it changes.
fn document(&self) -> Option<u64>;
/// Show the issue in the document, usually by selecting it.
fn reveal(&mut self, issue: &Issue);
/// Replace the issue's text with exactly `to`, as one undoable edit.
fn change(&mut self, issue: &Issue, to: &str) -> Result<(), String>;
/// Accept the word from now on.
fn add_word(&mut self, word: &str) -> Result<(), String>;
/// A line above the review about what it can't cover (a language with no data), if any.
fn notice(&self) -> Option<String> {
None
}
}

#[cfg(test)]
mod tests;
Loading