Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5854346053 | ||
|
|
367117ba26 | ||
|
|
5a09d1aef8 | ||
|
|
e1d560a727 | ||
|
|
07654ff45d | ||
|
|
53297259d2 | ||
|
|
ba93f62bcb | ||
|
|
0d68c1283a |
No files matched your search
Generated
+2
@@ -4312,8 +4312,10 @@ dependencies = [
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"vectorcraft-engine",
|
||||
"vectorcraft-text",
|
||||
"vectorcraft-ui-egui",
|
||||
"webbrowser",
|
||||
"windows",
|
||||
"winit",
|
||||
"winresource",
|
||||
]
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
|
||||
## Where we are
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ windows7 = ["eframe/glow"]
|
||||
[dependencies]
|
||||
vectorcraft-engine = { workspace = true }
|
||||
vectorcraft-ui-egui = { workspace = true }
|
||||
vectorcraft-text = { workspace = true }
|
||||
eframe = { workspace = true, features = ["default_fonts", "wayland", "x11"] }
|
||||
egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -36,6 +37,11 @@ winit = { version = "0.30", default-features = false }
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
clipboard-win = { workspace = true }
|
||||
|
||||
# DirectWrite's system font collection (src/system_fonts.rs): fonts that font services such as
|
||||
# Adobe Fonts load in place (#579). MIT OR Apache-2.0, already in the build through accesskit.
|
||||
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# The macOS menu bar (src/mac_menu.rs): Tauri's menu crate, AppKit menus behind a safe API (no GTK
|
||||
# with default features off). Pinned exactly: review the diff before bumping.
|
||||
|
||||
@@ -27,6 +27,8 @@ mod mac_menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
mod printing;
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
mod system_fonts;
|
||||
mod window;
|
||||
|
||||
use vectorcraft_engine::Session;
|
||||
@@ -314,6 +316,9 @@ fn main() -> std::process::ExitCode {
|
||||
// First, so every start-up warning is recorded (`logging`).
|
||||
let logger = logging::install();
|
||||
vectorcraft_ui_egui::i18n::detect_system_lang_in_background();
|
||||
// Before the first font scan (the app's start): the fonts font services load (#579).
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
system_fonts::install();
|
||||
let mut control_port: Option<u16> = std::env::var("VECTORCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
let mut in_window_menus = std::env::var_os("VECTORCRAFT_IN_WINDOW_MENUS").is_some_and(|v| !v.is_empty() && v != "0");
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Windows: the font files in DirectWrite's system font collection, scanned with the font folders
|
||||
//! (`vectorcraft_text::set_platform_font_files`).
|
||||
//!
|
||||
//! Font services such as Adobe Fonts load their fonts with GDI at run time, from files in their
|
||||
//! own folders, without installing them: the font folders and the registry don't list them. Since
|
||||
//! Windows 10 (version 1607) DirectWrite's system font collection holds them too
|
||||
//! (<https://learn.microsoft.com/windows/win32/directwrite/what-s-new-in-directwrite-for-windows-8-consumer-preview>,
|
||||
//! "Support for Adobe Typekit and other font-service clients"), so VectorCraft lists them as other
|
||||
//! apps do, reading the files in place (#579).
|
||||
//!
|
||||
//! DirectWrite is COM, which has no safe API, hence the scoped `unsafe_code` allowance.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use windows::Win32::Graphics::DirectWrite::{
|
||||
DWRITE_FACTORY_TYPE_SHARED, DWriteCreateFactory, IDWriteFactory3, IDWriteFontSet, IDWriteLocalFontFileLoader,
|
||||
};
|
||||
use windows::core::Interface;
|
||||
|
||||
/// The most fonts read from the collection (a system has a few thousand).
|
||||
const MAX_FONTS: u32 = 1 << 16;
|
||||
/// The longest file path taken, in UTF-16 units (Windows' own limit).
|
||||
const MAX_PATH: u32 = 32_767;
|
||||
|
||||
/// Have the font scans read the font files DirectWrite lists.
|
||||
pub fn install() {
|
||||
vectorcraft_text::set_platform_font_files(font_files);
|
||||
}
|
||||
|
||||
/// The local files of the fonts in DirectWrite's system font collection as it is now (fonts
|
||||
/// loaded since the last call included). None before Windows 10, or when DirectWrite fails: the
|
||||
/// font folders and the registry still list the installed fonts.
|
||||
fn font_files() -> Vec<String> {
|
||||
match collection_files() {
|
||||
Ok(files) => files.into_iter().collect(),
|
||||
Err(e) => {
|
||||
log::debug!("DirectWrite's system fonts aren't listed: {e}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collection_files() -> windows::core::Result<BTreeSet<String>> {
|
||||
// SAFETY: plain DirectWrite calls on interfaces it returned, each checked for failure; the
|
||||
// collection is asked to check for fonts loaded since the factory last looked.
|
||||
let set = unsafe {
|
||||
let factory: IDWriteFactory3 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?;
|
||||
let mut collection = None;
|
||||
factory.GetSystemFontCollection(false, &mut collection, true)?;
|
||||
let Some(collection) = collection else { return Ok(BTreeSet::new()) };
|
||||
collection.GetFontSet()?
|
||||
};
|
||||
// SAFETY: a count of the set's fonts.
|
||||
let count = unsafe { set.GetFontCount() }.min(MAX_FONTS);
|
||||
// Faces of one file (a collection's, a variable font's instances) name it once.
|
||||
Ok((0..count).filter_map(|i| local_path(&set, i)).collect())
|
||||
}
|
||||
|
||||
/// The path of the file holding font `i` of `set`, when it is a local file (not a downloadable
|
||||
/// font, nor one in memory).
|
||||
fn local_path(set: &IDWriteFontSet, i: u32) -> Option<String> {
|
||||
// SAFETY: `i` is below the set's count; the key DirectWrite hands back stays valid while
|
||||
// `file` (which owns it) lives, and is passed back with its own size; the path buffer is
|
||||
// sized from DirectWrite's answer plus the terminating NUL.
|
||||
unsafe {
|
||||
let file = set.GetFontFaceReference(i).ok()?.GetFontFile().ok()?;
|
||||
let loader: IDWriteLocalFontFileLoader = file.GetLoader().ok()?.cast().ok()?;
|
||||
let (mut key, mut size) = (std::ptr::null_mut(), 0u32);
|
||||
file.GetReferenceKey(&mut key, &mut size).ok()?;
|
||||
let len = loader.GetFilePathLengthFromKey(key, size).ok()?;
|
||||
if len == 0 || len > MAX_PATH {
|
||||
return None;
|
||||
}
|
||||
let mut path = vec![0u16; len as usize + 1];
|
||||
loader.GetFilePathFromKey(key, size, &mut path).ok()?;
|
||||
Some(String::from_utf16_lossy(path.get(..len as usize)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// DirectWrite lists the installed fonts, by their files' full paths.
|
||||
#[test]
|
||||
fn directwrite_lists_the_installed_font_files() {
|
||||
let files = super::font_files();
|
||||
assert!(files.iter().all(|f| std::path::Path::new(f).is_absolute()), "{:?}", files.first());
|
||||
let fonts = std::path::Path::new(&std::env::var("WINDIR").unwrap()).join("Fonts").to_string_lossy().to_lowercase();
|
||||
assert!(files.iter().any(|f| f.to_lowercase().starts_with(&fonts)), "{} files, {:?}", files.len(), files.first());
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,27 @@
|
||||
use harfrust::{Feature, Tag};
|
||||
use vectorcraft_doc::CharStyle;
|
||||
|
||||
/// Tracking (in 1/1000 em) outside which standard ligatures are dropped automatically: tighter
|
||||
/// than the first, looser than the second.
|
||||
///
|
||||
/// Letterspaced type should not ligate: an `fi` glyph keeps its letters tight while everything
|
||||
/// around it opens up (or stays open while it closes up), which reads as a blot. Small tracking
|
||||
/// adjustments made to fit a line are not letterspacing and keep the ligatures. The limits are
|
||||
/// about −8% and +22% of a typical word space (250/1000 em), the thresholds Adobe's composers are
|
||||
/// described as using (John Hudson, www-style, 2012-07): condensing shows the fixed ligature
|
||||
/// sooner than expanding. An explicit `liga` / `clig` in the character's own features always wins.
|
||||
pub const LIGATURE_TRACKING_LIMITS: (f64, f64) = (-20.0, 55.0);
|
||||
|
||||
/// OpenType features applied during shaping. Kerning, `case` and ligature suppression are driven
|
||||
/// by the character style (`kerning`, `all_caps`, `tracking`); the rest are layout-wide options.
|
||||
/// Standard ligatures are dropped when tracking is outside [`LIGATURE_TRACKING_LIMITS`], unless
|
||||
/// the character's features turn `liga` (or `clig`) on explicitly.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct OtFeatures {
|
||||
/// Vertical glyph alternates (`vert`), set by the writing direction.
|
||||
pub vertical: bool,
|
||||
/// Standard ligatures (`liga`, `clig`). Suppressed automatically when tracking is non-zero.
|
||||
/// Standard ligatures (`liga`, `clig`). Suppressed automatically when tracking is outside
|
||||
/// [`LIGATURE_TRACKING_LIMITS`], unless the character turns them on explicitly.
|
||||
pub ligatures: bool,
|
||||
/// Contextual alternates (`calt`).
|
||||
pub contextual: bool,
|
||||
@@ -117,7 +131,7 @@ impl OtFeatures {
|
||||
if st.kerning.is_some() {
|
||||
v.push(f(b"kern", false));
|
||||
}
|
||||
let liga = s.ligatures && st.tracking.abs() < 1e-9;
|
||||
let liga = s.ligatures && (!ligatures_suppressed_by(st.tracking) || explicit_ligatures(&st.features));
|
||||
if !liga {
|
||||
v.push(f(b"liga", false));
|
||||
v.push(f(b"clig", false));
|
||||
@@ -150,6 +164,28 @@ impl OtFeatures {
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `tracking` (1/1000 em) count as letterspacing that drops standard ligatures?
|
||||
pub fn ligatures_suppressed_by(tracking: f64) -> bool {
|
||||
// A non-finite value is not a real letterspacing request; keep the default.
|
||||
let (tight, loose) = LIGATURE_TRACKING_LIMITS;
|
||||
tracking.is_finite() && (tracking < tight || tracking > loose)
|
||||
}
|
||||
|
||||
/// Do `features` turn standard ligatures on explicitly (`liga`, `+liga`, `clig`, `+clig`)? The last
|
||||
/// ligature tag wins, matching [`OtFeatures::with_tags`]. Absence of a tag means "default", which
|
||||
/// [`OtFeatures::to_tags`] never writes, so this is distinguishable from an explicit request.
|
||||
pub fn explicit_ligatures<S: AsRef<str>>(features: &[S]) -> bool {
|
||||
features
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|t| match t.as_ref() {
|
||||
"liga" | "+liga" | "clig" | "+clig" => Some(true),
|
||||
"-liga" | "-clig" => Some(false),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -782,8 +782,29 @@ pub(crate) fn sfnt_of(tables: &[(&[u8; 4], &[u8])]) -> Option<Vec<u8>> {
|
||||
Some(font)
|
||||
}
|
||||
|
||||
/// The platform's font folders (the system's and the user's) and, on Windows, the font files
|
||||
/// registered with it outside them, scanned by [`FontDb::global`].
|
||||
/// Lists the font files the platform's font service knows, for [`set_platform_font_files`].
|
||||
pub type PlatformFontFiles = fn() -> Vec<String>;
|
||||
|
||||
/// What lists the platform's font files, set by the app ([`set_platform_font_files`]).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
static PLATFORM_FONT_FILES: std::sync::OnceLock<PlatformFontFiles> = std::sync::OnceLock::new();
|
||||
|
||||
/// Have [`system_font_dirs`] add the font files `list` gives (full paths) outside the font
|
||||
/// folders, asked again by each scan and each check for installed fonts
|
||||
/// ([`FontDb::installed_fonts_changed`]). The desktop app lists DirectWrite's system font
|
||||
/// collection on Windows: fonts a font service such as Adobe Fonts loads in place, from files
|
||||
/// outside the font folders and unknown to the registry (#579). Only the first call counts.
|
||||
pub fn set_platform_font_files(list: PlatformFontFiles) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
// A second call keeps the first lister, as documented.
|
||||
let _ = PLATFORM_FONT_FILES.set(list);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = list;
|
||||
}
|
||||
|
||||
/// The platform's font folders (the system's and the user's) and the font files known to it
|
||||
/// outside them (on Windows those registered with it, and those [`set_platform_font_files`]
|
||||
/// lists), scanned by [`FontDb::global`].
|
||||
pub fn system_font_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs: Vec<PathBuf> = Vec::new();
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
@@ -824,6 +845,11 @@ pub fn system_font_dirs() -> Vec<PathBuf> {
|
||||
// In a Flatpak sandbox, the host's fonts (system, local and the user's).
|
||||
dirs.extend(["/run/host/fonts", "/run/host/local-fonts", "/run/host/user-fonts"].map(Into::into));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(list) = PLATFORM_FONT_FILES.get() {
|
||||
let files = fonts_outside(list(), &dirs);
|
||||
dirs.extend(files);
|
||||
}
|
||||
dirs
|
||||
}
|
||||
|
||||
@@ -848,7 +874,7 @@ fn registered_font_files(font_dirs: &[PathBuf]) -> Vec<PathBuf> {
|
||||
/// The full paths among `registered` (font registrations' data) that aren't in `font_dirs`
|
||||
/// (ignoring case, as Windows paths do), sorted and deduplicated. File names alone are files in
|
||||
/// the Windows font folder, which is scanned anyway. The count is capped.
|
||||
#[cfg(any(windows, test))]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn fonts_outside(registered: impl IntoIterator<Item = String>, font_dirs: &[PathBuf]) -> Vec<PathBuf> {
|
||||
const MAX_REGISTERED: usize = 1 << 16;
|
||||
let lower = |p: &Path| PathBuf::from(p.to_string_lossy().to_lowercase());
|
||||
@@ -1154,6 +1180,7 @@ impl FontDb {
|
||||
let mut n = 0;
|
||||
let paths = self.font_paths.get();
|
||||
let mut modified_at = Vec::new();
|
||||
// Each file with whether it was named by itself rather than found in a folder.
|
||||
let mut files = Vec::new();
|
||||
let mut stack = paths.clone();
|
||||
// Each folder once, however links lead back to it.
|
||||
@@ -1166,7 +1193,7 @@ impl FontDb {
|
||||
modified_at.push((d.clone(), modified(&d)));
|
||||
// Not a folder: a font file named by itself (or nothing, yet).
|
||||
let Ok(rd) = std::fs::read_dir(&d) else {
|
||||
files.push(d);
|
||||
files.push((d, true));
|
||||
continue;
|
||||
};
|
||||
for e in rd.flatten() {
|
||||
@@ -1174,13 +1201,15 @@ impl FontDb {
|
||||
if p.is_dir() {
|
||||
stack.push(p);
|
||||
} else {
|
||||
files.push(p);
|
||||
files.push((p, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in files {
|
||||
for (p, named) in files {
|
||||
// A file named by itself is a font whatever its name (font services keep fonts in
|
||||
// files without an extension); one of a folder's only with a font's extension.
|
||||
let ext = p.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase());
|
||||
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
|
||||
if !named && !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
|
||||
continue;
|
||||
}
|
||||
for FaceStyle { family, style, keys, weight, italic, traits, .. } in file_face_names(&p) {
|
||||
|
||||
@@ -23,8 +23,10 @@ pub mod test_fonts;
|
||||
pub mod thread;
|
||||
|
||||
pub use craft_fonts::{CRAFT_FONTS, CraftFont};
|
||||
pub use features::OtFeatures;
|
||||
pub use fontdb::{FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, style_weight, system_font_dirs};
|
||||
pub use features::{LIGATURE_TRACKING_LIMITS, OtFeatures, explicit_ligatures, ligatures_suppressed_by};
|
||||
pub use fontdb::{
|
||||
FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, PlatformFontFiles, set_platform_font_files, style_weight, system_font_dirs,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
pub use layout::{layout, layout_with};
|
||||
pub use vectorcraft_doc::TextObject;
|
||||
|
||||
@@ -237,3 +237,29 @@ fn the_database_tells_when_fonts_were_installed_or_removed_since_its_scan() {
|
||||
assert_eq!(db.load_system_fonts(), 2);
|
||||
assert_eq!(db.styles(FAMILY), ["Bold"]);
|
||||
}
|
||||
|
||||
/// A font a font service loads from a file without an extension, outside the font folders.
|
||||
fn service_font() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("vc-sysfonts-{}-service", std::process::id())).join(".29457")
|
||||
}
|
||||
|
||||
/// The fonts a font service (Adobe Fonts on Windows, through DirectWrite) loads in place are
|
||||
/// cataloged: named by the platform's lister, read whatever their file is called (#579).
|
||||
#[test]
|
||||
fn fonts_the_platform_lists_outside_the_font_folders_are_cataloged() {
|
||||
let file = service_font();
|
||||
std::fs::create_dir_all(file.parent().unwrap()).unwrap();
|
||||
std::fs::write(&file, renamed_to("SourceSans3-Regular.ttf", "Service Sans3")).unwrap();
|
||||
// The lister's files outside the font folders are scanned with them, each once (a collection
|
||||
// lists its file once per face).
|
||||
fn lister() -> Vec<String> {
|
||||
vec![service_font().to_string_lossy().into_owned(); 2]
|
||||
}
|
||||
set_platform_font_files(lister);
|
||||
let dirs = system_font_dirs();
|
||||
assert_eq!(dirs.iter().filter(|d| **d == file).count(), 1, "{dirs:?}");
|
||||
// A file named by itself is read without a font's extension, one in a folder isn't.
|
||||
let db = FontDb::with_font_dirs(vec![file.clone()]);
|
||||
assert_eq!(db.styles("Service Sans3"), ["Regular"]);
|
||||
assert!(!FontDb::with_font_dirs(vec![file.parent().unwrap().to_path_buf()]).has_family("Service Sans3"));
|
||||
}
|
||||
@@ -370,9 +370,24 @@ fn opentype_ligature_switches() {
|
||||
assert_eq!(layout(db(), &t).glyphs.len(), 1, "fi ligature by default");
|
||||
let off = LayoutOptions { features: OtFeatures { ligatures: false, ..Default::default() }, ..Default::default() };
|
||||
assert_eq!(layout_with(db(), &t, &off).glyphs.len(), 2);
|
||||
// Tracking suppresses ligatures (as letterspaced type should).
|
||||
let tracked = TextObject::point(Point::ZERO, "fi", CharStyle { tracking: 100.0, ..serif });
|
||||
assert_eq!(layout(db(), &tracked).glyphs.len(), 2);
|
||||
let fi = |tracking: f64, features: &[&str]| {
|
||||
let st = CharStyle { tracking, features: features.iter().map(|s| s.to_string()).collect(), ..serif.clone() };
|
||||
layout(db(), &TextObject::point(Point::ZERO, "fi", st)).glyphs.len()
|
||||
};
|
||||
// Small tracking adjustments (fitting a line) keep the ligature.
|
||||
assert_eq!(fi(-5.0, &[]), 1, "tracking -5 keeps fi");
|
||||
assert_eq!(fi(20.0, &[]), 1, "tracking +20 keeps fi");
|
||||
let (tight, loose) = LIGATURE_TRACKING_LIMITS;
|
||||
assert_eq!((fi(tight, &[]), fi(loose, &[])), (1, 1), "the limits themselves keep fi");
|
||||
// Real letterspacing suppresses ligatures (as letterspaced type should), condensing sooner...
|
||||
assert_eq!(fi(100.0, &[]), 2, "tracking 100 drops fi");
|
||||
assert_eq!(fi(-30.0, &[]), 2, "tracking -30 drops fi");
|
||||
assert_eq!(fi(-100.0, &[]), 2, "tracking -100 drops fi");
|
||||
// ...unless the character turns them on explicitly.
|
||||
assert_eq!(fi(100.0, &["liga"]), 1, "explicit liga wins over tracking");
|
||||
assert_eq!(fi(100.0, &["+liga"]), 1);
|
||||
assert_eq!(fi(100.0, &["liga", "-liga"]), 2, "the last tag wins");
|
||||
assert_eq!(fi(0.0, &["-liga"]), 2, "explicit off still turns it off");
|
||||
let f = OtFeatures::from_tags(["dlig", "-liga", "smcp", "onum", "bogus"]);
|
||||
assert!(f.discretionary_ligatures && !f.ligatures && f.small_caps && f.oldstyle_figures && !f.fractions);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ use crate::widgets::{self, Live};
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec::window(show, confirm);
|
||||
|
||||
/// The field holding the frame the popover was first drawn in.
|
||||
const OPENED: &str = "__opened";
|
||||
|
||||
/// What the popover edits: the selected stop of the gradient behind the active proxy (with all
|
||||
/// its stops), or the selected point of a freeform gradient.
|
||||
enum Target {
|
||||
@@ -131,12 +134,18 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
});
|
||||
})
|
||||
.response;
|
||||
if let (Some(tab), Some(d)) = (tab, app.ui.dialog.as_mut()) {
|
||||
d.fields.insert("tab".into(), json!(tab));
|
||||
// The frame it opened in (its first drawn).
|
||||
let frame = ctx.cumulative_frame_nr();
|
||||
let opened = d.fields.get(OPENED).and_then(Value::as_u64).unwrap_or(frame);
|
||||
if let Some(d) = app.ui.dialog.as_mut() {
|
||||
if let Some(tab) = tab {
|
||||
d.fields.insert("tab".into(), json!(tab));
|
||||
}
|
||||
d.fields.insert(OPENED.into(), json!(opened));
|
||||
}
|
||||
// The double-click that opened the popover lands on its chip, outside it: only a later click
|
||||
// elsewhere closes it.
|
||||
if resp.clicked_elsewhere() && !ctx.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)) {
|
||||
// The click that opened the popover (the Gradient panel's colour chip) or the double-click
|
||||
// (a stop's chip) lands outside it: only a later click elsewhere closes it (#577).
|
||||
if frame > opened && resp.clicked_elsewhere() && !ctx.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)) {
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
}
|
||||
@@ -246,10 +255,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
// One context per test, so frames follow one another as in the app.
|
||||
static CTX: egui::Context = {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
ctx
|
||||
};
|
||||
}
|
||||
|
||||
/// One headless frame of the dialog layer and the shortcuts.
|
||||
fn frame(app: &mut VectorcraftApp, events: Vec<egui::Event>) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let ctx = CTX.with(Clone::clone);
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
crate::shortcuts::handle(app, ui.ctx());
|
||||
super::super::show(app, ui.ctx());
|
||||
@@ -312,6 +329,25 @@ mod tests {
|
||||
assert!(app.ui.dialog.is_none(), "a click elsewhere closes it");
|
||||
}
|
||||
|
||||
/// #577: the Gradient panel's colour chip opens it with a click whose release lands in the
|
||||
/// frame it opens in, outside it: that click doesn't close it.
|
||||
#[test]
|
||||
fn the_click_that_opens_it_from_the_panel_keeps_it_open() {
|
||||
let mut app = app();
|
||||
let at = egui::pos2(5.0, 5.0);
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(at), click(at, true)]);
|
||||
// The panel opens it while the click is released.
|
||||
app.ui.dialog = Some(Dialog::new("gradientStop", json!({"index": 1, "screen": [200.0, 200.0], "tab": "color"})));
|
||||
frame(&mut app, vec![click(at, false)]);
|
||||
for _ in 0..3 {
|
||||
frame(&mut app, vec![]);
|
||||
}
|
||||
assert!(app.ui.dialog.is_some(), "the opening click must not close it");
|
||||
let away = egui::pos2(5.0, 600.0);
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(away), click(away, true), click(away, false)]);
|
||||
assert!(app.ui.dialog.is_none(), "a later click elsewhere closes it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_popover_closes_when_its_stop_goes_away() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -347,6 +347,9 @@ pub struct VectorcraftApp {
|
||||
/// The thread that checks the system clipboard for `system_paste` instead
|
||||
/// ([`Services::clipboard_probe`]).
|
||||
clipboard_probe: Option<clipboard_probe::Probe>,
|
||||
/// The look for fonts installed or removed while the app was in the background, running
|
||||
/// ([`Self::refresh_installed_fonts`]): whether they were.
|
||||
font_check: Option<std::sync::mpsc::Receiver<bool>>,
|
||||
/// Keyboard pastes of something other than text (see [`shortcuts::PasteChord`]).
|
||||
pub(crate) paste_chord: shortcuts::PasteChord,
|
||||
/// Saves and exports running in the background (Preferences → File Handling).
|
||||
@@ -423,6 +426,7 @@ impl VectorcraftApp {
|
||||
system_paste: false,
|
||||
system_paste_at: f64::NEG_INFINITY,
|
||||
clipboard_probe: None,
|
||||
font_check: None,
|
||||
paste_chord: Default::default(),
|
||||
background: Default::default(),
|
||||
recovery: Default::default(),
|
||||
@@ -785,6 +789,7 @@ impl VectorcraftApp {
|
||||
self.system_paste_at = now;
|
||||
self.system_paste = wanted && self.system_clipboard_pasteable();
|
||||
}
|
||||
self.poll_font_check(ctx);
|
||||
background::poll(self);
|
||||
if !self.background.jobs.is_empty() {
|
||||
// Keep the status bar's progress moving and pick the result up when it arrives.
|
||||
@@ -858,10 +863,38 @@ impl VectorcraftApp {
|
||||
|
||||
/// Fonts installed or removed while the app was in the background are listed when it comes
|
||||
/// back (Refresh Font List by itself): a look at the font folders, a scan only when they changed.
|
||||
/// The look runs on another thread (asking DirectWrite for the fonts font services loaded
|
||||
/// meanwhile takes tens of milliseconds on Windows, #579), [`Self::poll_font_check`] scans.
|
||||
fn refresh_installed_fonts(&mut self) {
|
||||
if vectorcraft_text::FontDb::global().installed_fonts_changed() {
|
||||
// A failure shows in the status bar, as the menu item's does.
|
||||
let _ = self.run("text.rescanFonts", json!({}));
|
||||
if self.font_check.is_some() {
|
||||
return;
|
||||
}
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let look = move || {
|
||||
// The UI gone meanwhile has nothing to refresh.
|
||||
let _ = tx.send(vectorcraft_text::FontDb::global().installed_fonts_changed());
|
||||
};
|
||||
let spawned =
|
||||
if cfg!(target_arch = "wasm32") { None } else { std::thread::Builder::new().name("font-check".into()).spawn(look.clone()).ok() };
|
||||
if spawned.is_none() {
|
||||
look();
|
||||
}
|
||||
self.font_check = Some(rx);
|
||||
}
|
||||
|
||||
/// Rescan the fonts once [`Self::refresh_installed_fonts`]'s look says they changed.
|
||||
fn poll_font_check(&mut self, ctx: &egui::Context) {
|
||||
let Some(check) = &self.font_check else { return };
|
||||
match check.try_recv() {
|
||||
Ok(changed) => {
|
||||
self.font_check = None;
|
||||
if changed {
|
||||
// A failure shows in the status bar, as the menu item's does.
|
||||
let _ = self.run("text.rescanFonts", json!({}));
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => ctx.request_repaint_after(std::time::Duration::from_millis(50)),
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => self.font_check = None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
*field(&mut next) = !on;
|
||||
}
|
||||
}
|
||||
if st.tracking.abs() > 1e-9 && cur.ligatures {
|
||||
if cur.ligatures && vectorcraft_text::ligatures_suppressed_by(st.tracking) && !vectorcraft_text::explicit_ligatures(&st.features) {
|
||||
widgets::dim_label(ui, tl!("Ligatures are off while the text is tracked."));
|
||||
}
|
||||
if next != cur {
|
||||
|
||||
+2
-1
@@ -2310,7 +2310,8 @@ tone (mono, or CMYK screens multiplied over each other), clipped to the art's ou
|
||||
|
||||
Type can use the bundled fonts, fonts added to the session and the fonts installed on the system (none on the web):
|
||||
the system's and the user's font folders (Windows: `Fonts` and `%LOCALAPPDATA%\Microsoft\Windows\Fonts`, plus fonts
|
||||
registered outside them, such as fonts installed as shortcuts; macOS: `/System/Library/Fonts`, `/Library/Fonts`,
|
||||
registered outside them, such as fonts installed as shortcuts, and in the desktop app the fonts in DirectWrite's system
|
||||
font collection, such as those Adobe Fonts activates while Creative Cloud runs; macOS: `/System/Library/Fonts`, `/Library/Fonts`,
|
||||
`/Network/Library/Fonts`, `~/Library/Fonts` and downloaded system fonts; Linux and BSD: `/usr/share/fonts`,
|
||||
`/usr/local/share/fonts`, `~/.fonts` and the XDG data folders' `fonts`, `~/.local/share/fonts` among them, and in a
|
||||
Flatpak sandbox the host's fonts). The installed fonts are cataloged once per session (in the background when the app starts, else on the first lookup
|
||||
|
||||
Reference in new issue
Block a user