mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Merge pull request #810 from Messhias/feat/394-hyphenation-exceptions
Part of #394: Hyphenation › Exceptions reach the hyphenator
This commit is contained in:
7 files changed
+243
-2
No files matched your search
+2
-2
@@ -114,7 +114,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas.
|
||||
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
|
||||
- Four brightness themes, persistent preferences.
|
||||
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). About 20 stored options still do nothing yet (the rest of #394: Show Font Names in English, Highlight Alternate Glyphs, Display Print Size at 100% Zoom, Identify Objects By, East Asian Type units, Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation, scratch disks, the legacy clipboard, Appearance of Black, Touch Workspace).
|
||||
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). About 19 stored options still do nothing yet (the rest of #394: Show Font Names in English, Highlight Alternate Glyphs, Display Print Size at 100% Zoom, Identify Objects By, East Asian Type units, Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Appearance of Black, Touch Workspace).
|
||||
- Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423).
|
||||
- Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once.
|
||||
- Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off.
|
||||
@@ -232,7 +232,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 |
|
||||
| Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 |
|
||||
| Automation | 3 | 60% | Variables (data merge), scripting surface, batch | 8–12 |
|
||||
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | macOS native menu dismissal fixed (M14.8); remaining: Variables, SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas (Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done), about 20 Preferences options stored but not yet acted on (#394: Spacing Guides, Show Font Names in English, Highlight Alternate Glyphs, Display Print Size, Open Documents As Tabs, Real-time Drawing, hyphenation, Appearance of Black, devices…) | 14–22 |
|
||||
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | macOS native menu dismissal fixed (M14.8); remaining: Variables, SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas (Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done), about 19 Preferences options stored but not yet acted on (#394: Show Font Names in English, Highlight Alternate Glyphs, Display Print Size, Open Documents As Tabs, Real-time Drawing, hyphenation language, Appearance of Black, devices…) | 14–22 |
|
||||
| Libraries, Links, Package | 2 | 80% | the Libraries panel is local only (no cloud by design: libraries are files on this machine) and has no shared libraries, linked graphics or gradients and patterns as items yet; its graphics, colours and text styles are done (#745); Links and Package are done; placed VectorCraft documents are done (linked by default, vectors in every output, previews, Break Link, Expand, Edit Original, refresh on save) | 2–3 |
|
||||
| **Feature parity** | **103** | **~75%** | | **~210–330** |
|
||||
| Interaction-fidelity pass (side by side with Illustrator: every tool modifier, cursor, dialog, Properties context) | | | | 60–90 |
|
||||
|
||||
@@ -475,6 +475,7 @@ impl Session {
|
||||
let history_changed = p.history_states != self.prefs.history_states;
|
||||
let tile_edge_changed = p.pattern_tile_edge_color != self.prefs.pattern_tile_edge_color;
|
||||
let fonts_folder_changed = p.fonts_folder != self.prefs.fonts_folder;
|
||||
let hyphen_exceptions_changed = p.hyphenation_exceptions != self.prefs.hyphenation_exceptions;
|
||||
self.prefs = p;
|
||||
if fonts_folder_changed {
|
||||
let folder = self.prefs.fonts_folder.trim();
|
||||
@@ -486,6 +487,10 @@ impl Session {
|
||||
let _ = super::fonts::rescan(self, &serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if hyphen_exceptions_changed {
|
||||
// Preferences › Hyphenation › Exceptions (#394): the layout reads the process-wide list.
|
||||
vectorcraft_text::set_hyphenation_exceptions(&self.prefs.hyphenation_exceptions);
|
||||
}
|
||||
vectorcraft_render::set_default_threads(u16::try_from(self.prefs.render_threads).ok());
|
||||
for st in &mut self.docs {
|
||||
if history_changed {
|
||||
@@ -503,6 +508,10 @@ impl Session {
|
||||
d.grid.subdivisions = self.prefs.grid_subdivisions;
|
||||
st.revision += 1;
|
||||
}
|
||||
// Hyphenation exceptions change every text object's line breaks.
|
||||
if hyphen_exceptions_changed {
|
||||
st.revision += 1;
|
||||
}
|
||||
}
|
||||
// Plug-ins in a newly set Additional Plug-ins Folder are installed.
|
||||
super::plugin::sync_prefs(self);
|
||||
|
||||
@@ -670,3 +670,27 @@ world", "size": 20}),
|
||||
let NodeKind::Text(t) = &s.doc().unwrap().doc.node(dst).unwrap().kind else { panic!("not type") };
|
||||
assert_eq!((t.first_style().size, t.first_style().fill.color().map(|c| c.to_hex())), (20.0, Some("#00ff00".into())));
|
||||
}
|
||||
|
||||
/// Preferences › Hyphenation › Exceptions (#394): `prefs.set` pushes the list into the hyphenator
|
||||
/// and bumps open documents so type reflows. Parsing of the list is covered in `vectorcraft-text`.
|
||||
#[test]
|
||||
fn hyphenation_exceptions_preference_reaches_the_hyphenator() {
|
||||
struct Clear;
|
||||
impl Drop for Clear {
|
||||
fn drop(&mut self) {
|
||||
vectorcraft_text::set_hyphenation_exceptions("");
|
||||
}
|
||||
}
|
||||
let _clear = Clear;
|
||||
let mut s = Session::new();
|
||||
set_pref(&mut s, "hyphenationExceptions", json!(""));
|
||||
assert!(vectorcraft_text::hyphenation_exceptions().is_empty());
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let before = s.doc().unwrap().revision;
|
||||
set_pref(&mut s, "hyphenationExceptions", json!("typography, hap-pen"));
|
||||
assert_eq!(s.prefs.hyphenation_exceptions, "typography, hap-pen");
|
||||
assert_eq!(vectorcraft_text::hyphenation_exceptions(), "typography, hap-pen");
|
||||
assert!(s.doc().unwrap().revision > before, "type reflows");
|
||||
set_pref(&mut s, "hyphenationExceptions", json!(""));
|
||||
assert!(vectorcraft_text::hyphenation_exceptions().is_empty());
|
||||
}
|
||||
@@ -1,12 +1,108 @@
|
||||
//! Pattern-free English hyphenation: break long words between syllables at vowel–consonant
|
||||
//! boundaries (V-CV: "ty-po", and VC-CV: "hap-pen"), never leaving fewer than [`MIN_BEFORE`]
|
||||
//! letters before or [`MIN_AFTER`] after the hyphen.
|
||||
//!
|
||||
//! Preferences › Hyphenation › Exceptions (comma separated) override the points for named words:
|
||||
//! `typography` never breaks, `ty-pog-ra-phy` breaks only where the hyphens mark. See
|
||||
//! [`set_hyphenation_exceptions`].
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
|
||||
/// Only words at least this long are hyphenated (Illustrator's default: longer than 5 letters).
|
||||
pub const MIN_WORD: usize = 6;
|
||||
pub const MIN_BEFORE: usize = 3;
|
||||
pub const MIN_AFTER: usize = 3;
|
||||
|
||||
/// An exception for one word: never break it, or break only at the listed character indices.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum Exception {
|
||||
Never,
|
||||
Only(Vec<usize>),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Exceptions {
|
||||
source: String,
|
||||
map: HashMap<String, Exception>,
|
||||
}
|
||||
|
||||
fn store() -> MutexGuard<'static, Exceptions> {
|
||||
static LOCK: OnceLock<Mutex<Exceptions>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(Exceptions::default())).lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn parse_list(source: &str) -> Exceptions {
|
||||
let mut map = HashMap::new();
|
||||
for entry in source.split(',') {
|
||||
let entry = entry.trim();
|
||||
if entry.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (word, ex) = parse_exception(entry);
|
||||
if word.is_empty() {
|
||||
continue;
|
||||
}
|
||||
map.insert(word, ex);
|
||||
}
|
||||
Exceptions { source: source.to_string(), map }
|
||||
}
|
||||
|
||||
/// Preferences › Hyphenation › Exceptions: comma-separated words. A word with no hyphens is never
|
||||
/// hyphenated; hyphens mark the only allowed break points (`ty-pog-ra-phy`). Matching is
|
||||
/// case-insensitive. Empty clears the list.
|
||||
pub fn set_hyphenation_exceptions(source: &str) {
|
||||
*store() = parse_list(source);
|
||||
}
|
||||
|
||||
/// The last string passed to [`set_hyphenation_exceptions`] (empty when none).
|
||||
pub fn hyphenation_exceptions() -> String {
|
||||
store().source.clone()
|
||||
}
|
||||
|
||||
fn parse_exception(entry: &str) -> (String, Exception) {
|
||||
// Illustrator: bare word = never break; hyphens mark the only allowed breaks. No other syntax.
|
||||
if !entry.contains('-') {
|
||||
return (normalize_word(entry), Exception::Never);
|
||||
}
|
||||
let mut word = String::new();
|
||||
let mut points = Vec::new();
|
||||
for ch in entry.chars() {
|
||||
if ch == '-' {
|
||||
let at = word.chars().count();
|
||||
if at > 0 {
|
||||
points.push(at);
|
||||
}
|
||||
} else {
|
||||
for c in ch.to_lowercase() {
|
||||
word.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
let n = word.chars().count();
|
||||
points.retain(|&p| p > 0 && p < n);
|
||||
points.sort_unstable();
|
||||
points.dedup();
|
||||
(word, Exception::Only(points))
|
||||
}
|
||||
|
||||
fn normalize_word(word: &str) -> String {
|
||||
word.chars().flat_map(|c| c.to_lowercase()).collect()
|
||||
}
|
||||
|
||||
/// Look the word up under the preferences lock (or the thread-local test override). Only the matched
|
||||
/// [`Exception`] is cloned, never the whole list.
|
||||
fn exception_for(word: &str) -> Option<Exception> {
|
||||
let key = normalize_word(word);
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let Some(ex) = TEST_OVERRIDE.with(|c| c.borrow().as_ref().and_then(|e| e.map.get(&key).cloned())) {
|
||||
return Some(ex);
|
||||
}
|
||||
}
|
||||
store().map.get(&key).cloned()
|
||||
}
|
||||
|
||||
fn is_vowel(c: char) -> bool {
|
||||
matches!(c.to_ascii_lowercase(), 'a' | 'e' | 'i' | 'o' | 'u' | 'y') || (c.is_alphabetic() && !c.is_ascii())
|
||||
}
|
||||
@@ -25,7 +121,14 @@ fn onset_pair(a: char, b: char) -> bool {
|
||||
|
||||
/// Allowed hyphenation points in `word`, as char indices (a hyphen goes *before* the char).
|
||||
/// Words containing non-letters (other than a trailing apostrophe) are not hyphenated.
|
||||
/// Preferences exceptions override the pattern for a matching word.
|
||||
pub fn hyphen_points(word: &str) -> Vec<usize> {
|
||||
if let Some(ex) = exception_for(word) {
|
||||
return match ex {
|
||||
Exception::Never => vec![],
|
||||
Exception::Only(pts) => pts,
|
||||
};
|
||||
}
|
||||
let c: Vec<char> = word.chars().collect();
|
||||
let n = c.len();
|
||||
if n < MIN_WORD || !c.iter().all(|c| c.is_alphabetic() || matches!(c, '\'' | '’')) {
|
||||
@@ -69,3 +172,63 @@ pub fn hyphenate_word(word: &str) -> String {
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Per-thread list so unit tests don't race the process-wide preferences store.
|
||||
static TEST_OVERRIDE: std::cell::RefCell<Option<Exceptions>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Run `f` with Preferences › Hyphenation › Exceptions applied on this thread only (tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_hyphenation_exceptions_for_test(source: &str, f: impl FnOnce()) {
|
||||
TEST_OVERRIDE.with(|c| *c.borrow_mut() = Some(parse_list(source)));
|
||||
f();
|
||||
TEST_OVERRIDE.with(|c| *c.borrow_mut() = None);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn with_exceptions(source: &str, f: impl FnOnce()) {
|
||||
with_hyphenation_exceptions_for_test(source, f);
|
||||
}
|
||||
|
||||
/// #394 Preferences › Hyphenation › Exceptions contract (parse + override).
|
||||
#[test]
|
||||
fn hyphenation_exceptions_override_the_pattern() {
|
||||
let pattern = hyphen_points("typography");
|
||||
assert_eq!(pattern, vec![4, 7], "pattern: typo-gra-phy");
|
||||
|
||||
// Bare word: never break (case-insensitive).
|
||||
with_exceptions("typography", || {
|
||||
assert!(hyphen_points("typography").is_empty());
|
||||
assert!(hyphen_points("Typography").is_empty(), "case-insensitive");
|
||||
});
|
||||
|
||||
// Hyphens mark the only allowed breaks (stricter than the pattern).
|
||||
with_exceptions("typo-graphy", || {
|
||||
assert_eq!(hyphen_points("typography"), vec![4], "only the marked break");
|
||||
assert_eq!(hyphenate_word("typography"), "typo-graphy");
|
||||
});
|
||||
|
||||
// Comma-separated list; empty / whitespace entries ignored.
|
||||
with_exceptions(" , typography , hap-pen , ", || {
|
||||
assert!(hyphen_points("typography").is_empty());
|
||||
assert_eq!(hyphen_points("happen"), vec![3]);
|
||||
assert!(!hyphen_points("hyphenation").is_empty(), "unlisted words still use the pattern");
|
||||
});
|
||||
|
||||
assert_eq!(hyphen_points("typography"), pattern, "override cleared");
|
||||
}
|
||||
|
||||
/// Process-wide store used by `prefs.set` / restore.
|
||||
#[test]
|
||||
fn set_hyphenation_exceptions_round_trips_the_source_string() {
|
||||
set_hyphenation_exceptions("typography, hap-pen");
|
||||
assert_eq!(hyphenation_exceptions(), "typography, hap-pen");
|
||||
set_hyphenation_exceptions("");
|
||||
assert!(hyphenation_exceptions().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub use fontdb::{
|
||||
FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, IcfMargins, PlatformFontFiles, set_platform_font_files, set_user_font_dirs,
|
||||
style_weight, system_font_dirs, user_font_dirs,
|
||||
};
|
||||
pub use hyphen::{hyphenation_exceptions, set_hyphenation_exceptions};
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
pub use layout::{layout, layout_with};
|
||||
pub use vectorcraft_doc::TextObject;
|
||||
|
||||
@@ -415,6 +415,26 @@ fn hyphenated_layout_adds_hyphens() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Preferences › Hyphenation › Exceptions (#394): listing a word stops the layout from breaking it.
|
||||
#[test]
|
||||
fn hyphenation_exceptions_keep_listed_words_whole_in_layout() {
|
||||
// Same copy as `hyphenated_layout_adds_hyphens` (known to produce soft hyphens at 120pt width).
|
||||
let text = "Extraordinarily comprehensive typographical considerations notwithstanding everything.";
|
||||
let mut t = area(text, style(14.0), Rect::new(0.0, 0.0, 120.0, 600.0), Justify::Left);
|
||||
t.para.hyphenate = true;
|
||||
let with_breaks = layout(db(), &t);
|
||||
let hyphens = |l: &TextLayout| l.glyphs.iter().filter(|g| g.len == 0).count();
|
||||
assert!(hyphens(&with_breaks) > 0, "pattern produces soft hyphens");
|
||||
hyphen::with_hyphenation_exceptions_for_test(
|
||||
"extraordinarily, comprehensive, typographical, considerations, notwithstanding, everything",
|
||||
|| {
|
||||
let blocked = layout(db(), &t);
|
||||
assert_eq!(hyphens(&blocked), 0, "every long word listed: no soft hyphens");
|
||||
assert!(blocked.lines.len() >= with_breaks.lines.len());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soft_hyphen_is_invisible_mid_line() {
|
||||
let l = layout(db(), &TextObject::point(Point::ZERO, "co\u{00AD}operate", style(20.0)));
|
||||
|
||||
@@ -577,4 +577,28 @@ mod tests {
|
||||
assert!(PREF_SPECS.iter().any(|s| s.category == *c));
|
||||
}
|
||||
}
|
||||
|
||||
/// Preferences › Hyphenation › Exceptions (#394): OK on the dialog pushes the list into the
|
||||
/// hyphenator the same way `prefs.set` does (no need to drive the live Preferences window).
|
||||
#[test]
|
||||
fn hyphenation_exceptions_dialog_ok_reaches_the_hyphenator() {
|
||||
struct Clear;
|
||||
impl Drop for Clear {
|
||||
fn drop(&mut self) {
|
||||
vectorcraft_text::set_hyphenation_exceptions("");
|
||||
}
|
||||
}
|
||||
let _clear = Clear;
|
||||
let mut a = app();
|
||||
open(&mut a, Some("Hyphenation"));
|
||||
let d = a.ui.dialog.as_mut().unwrap();
|
||||
assert_eq!(d.str("__category"), "Hyphenation");
|
||||
d.fields.insert("hyphenationExceptions".into(), json!("typography, hap-pen"));
|
||||
confirm(&mut a).unwrap();
|
||||
assert!(a.ui.dialog.is_none());
|
||||
assert_eq!(a.session.prefs.hyphenation_exceptions, "typography, hap-pen");
|
||||
assert_eq!(vectorcraft_text::hyphenation_exceptions(), "typography, hap-pen");
|
||||
assert!(vectorcraft_text::hyphen::hyphen_points("typography").is_empty());
|
||||
assert_eq!(vectorcraft_text::hyphen::hyphen_points("happen"), vec![3]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user