Land #568 (fix #563), #570 (fix #530), #623 (part of #579)
This commit is contained in:
commit
8b81d58845
|
|
@ -4352,6 +4352,7 @@ dependencies = [
|
|||
name = "vectorcraft-cli"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde_json",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
|
|
@ -4362,6 +4363,8 @@ dependencies = [
|
|||
"vectorcraft-render",
|
||||
"vectorcraft-svg",
|
||||
"vectorcraft-testkit",
|
||||
"vectorcraft-text",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,7 +22,14 @@ vectorcraft-geom = { workspace = true }
|
|||
vectorcraft-svg = { workspace = true }
|
||||
vectorcraft-mcp = { workspace = true }
|
||||
vectorcraft-render = { workspace = true }
|
||||
vectorcraft-text = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
# DirectWrite's system font collection (the desktop app's src/system_fonts.rs, shared): fonts that
|
||||
# font services such as Adobe Fonts load in place (#579). MIT OR Apache-2.0.
|
||||
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite"] }
|
||||
|
||||
[dev-dependencies]
|
||||
vectorcraft-testkit = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
//! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5]
|
||||
//! vectorcraft-cli perf [--paths 50000]
|
||||
//! ```
|
||||
#![forbid(unsafe_code)]
|
||||
// Denied, not forbidden: the DirectWrite font lister shared with the desktop app (Windows) allows
|
||||
// it for its COM calls, and nothing else may.
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
use std::io::Write;
|
||||
use std::process::ExitCode;
|
||||
|
|
@ -35,6 +37,10 @@ macro_rules! out {
|
|||
}
|
||||
|
||||
mod perf;
|
||||
/// The desktop app's DirectWrite font lister, shared: exports and MCP find the same fonts.
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
#[path = "../../vectorcraft/src/system_fonts.rs"]
|
||||
mod system_fonts;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_mcp::{Backend, DEFAULT_ADDR, Headless, Remote, Server};
|
||||
|
|
@ -93,6 +99,9 @@ fn usage() -> String {
|
|||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
// Before any font lookup: the fonts font services load (#579).
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
system_fonts::install();
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let r = match args.first().map(String::as_str) {
|
||||
Some("mcp") => mcp(&args[1..]),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Windows: the font files in DirectWrite's system font collection, scanned with the font folders
|
||||
//! (`vectorcraft_text::set_platform_font_files`).
|
||||
//! (`vectorcraft_text::set_platform_font_files`). The desktop app and `vectorcraft-cli` (exports,
|
||||
//! MCP) both install it; the CLI shares this file.
|
||||
//!
|
||||
//! 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
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ struct MenuState {
|
|||
/// The pass the list was last drawn in, and the pass it opened in.
|
||||
last: u64,
|
||||
opened: u64,
|
||||
/// The family the text had as the menu opened. A preview shows another one in the document
|
||||
/// (and the menu's caller), but this one stays the current family: hovering it ends the
|
||||
/// preview (#563).
|
||||
current: String,
|
||||
/// The highlighted row and the query and filters it belongs to.
|
||||
highlight: Option<usize>,
|
||||
query: String,
|
||||
|
|
@ -194,6 +198,7 @@ fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, samp
|
|||
st.opened = pass;
|
||||
st.highlight = None;
|
||||
st.query.clear();
|
||||
st.current = current.to_string();
|
||||
}
|
||||
st.last = pass;
|
||||
// For [`end_stale_preview`]: the menu is open, in this layer.
|
||||
|
|
@ -243,8 +248,9 @@ fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, samp
|
|||
});
|
||||
let enter = ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
// The current family by its own name (a document may name it in Japanese: ヒラギノ角ゴシック).
|
||||
let current = db.canonical(current, "").0;
|
||||
// The current family by its own name (a document may name it in Japanese: ヒラギノ角ゴシック),
|
||||
// as it was before any preview.
|
||||
let current = db.canonical(&st.current, "").0;
|
||||
let current = current.as_str();
|
||||
let favorite = |f: &str| look.favorites.iter().any(|x| x.eq_ignore_ascii_case(f));
|
||||
// The generation is read first: fonts that load meanwhile make the next frame list them.
|
||||
|
|
|
|||
|
|
@ -1492,6 +1492,8 @@
|
|||
New Art Brush from Selection Nuevo pincel de arte a partir de la selección
|
||||
New Art Has Basic Appearance La nueva ilustración tiene apariencia básica
|
||||
New Artboard Nueva mesa de trabajo
|
||||
Artboard Mesa de trabajo
|
||||
Exit Salir
|
||||
New Bristle Brush Nuevo pincel de cerdas
|
||||
New Brush… Nuevo pincel…
|
||||
New Calligraphic Brush Nuevo pincel caligráfico
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1492,6 +1492,8 @@
|
|||
New Art Brush from Selection Nuovo pennello artistico dalla selezione
|
||||
New Art Has Basic Appearance La nuova grafica ha aspetto di base
|
||||
New Artboard Nuova tavola da disegno
|
||||
Artboard Tavola da disegno
|
||||
Exit Esci
|
||||
New Bristle Brush Nuovo pennello a setole
|
||||
New Brush… Nuovo pennello…
|
||||
New Calligraphic Brush Nuovo pennello calligrafico
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1635,6 +1635,8 @@
|
|||
New Art Brush from Selection 選択範囲から新規アートブラシ
|
||||
New Art Has Basic Appearance 新規アートに基本アピアランスを使用
|
||||
New Artboard 新規アートボード
|
||||
Artboard アートボード
|
||||
Exit 終了
|
||||
New Bristle Brush 新規剛毛ブラシ
|
||||
New Brush… 新規ブラシ…
|
||||
New Calligraphic Brush 新規絵筆ブラシ
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3604,3 +3604,5 @@ axis Both Оба
|
|||
Constrain Ограничить
|
||||
Free Transform Свободное трансформирование
|
||||
Perspective Distort Искажение перспективы
|
||||
Artboard Монтажная область
|
||||
Exit Выйти
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1469,6 +1469,7 @@
|
|||
New Art Brush from Selection 从选区新建艺术画笔
|
||||
New Art Has Basic Appearance 新建图稿使用基本外观
|
||||
New Artboard 新建画板
|
||||
Exit 退出
|
||||
New Bristle Brush 新建鬃毛画笔
|
||||
New Brush… 新建画笔…
|
||||
New Calligraphic Brush 新建书法画笔
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1509,6 +1509,8 @@
|
|||
New Art Brush from Selection 從選取範圍新增藝術筆刷
|
||||
New Art Has Basic Appearance 新圖稿具有基本外觀
|
||||
New Artboard 新增工作區域
|
||||
Artboard 工作區域
|
||||
Exit 結束
|
||||
New Bristle Brush 新增鬃毛筆刷
|
||||
New Brush… 新增筆刷…
|
||||
New Calligraphic Brush 新增書法筆刷
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -13,13 +13,13 @@ use crate::widgets::{self, menu_item};
|
|||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
/// The active artboard (of `n`).
|
||||
fn selected(app: &VectorcraftApp, n: usize) -> usize {
|
||||
pub(crate) fn selected(app: &VectorcraftApp, n: usize) -> usize {
|
||||
app.view().map_or(0, |v| v.artboard).min(n.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// Make artboard `i` the active one, leaving the view where it is: the navigator's and, while it
|
||||
/// is the tool, the Artboard tool's.
|
||||
fn select(app: &mut VectorcraftApp, i: usize) {
|
||||
pub(crate) fn select(app: &mut VectorcraftApp, i: usize) {
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = i;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let n_sel = st.selection.len();
|
||||
let anchors = crate::chrome::anchor_controls(app);
|
||||
let first = first_selected(app);
|
||||
// Edit Artboards (the Artboard tool): the active artboard, whatever is selected.
|
||||
let editing_artboards = app.session.tool_id() == "artboard";
|
||||
let label = match (&first, n_sel) {
|
||||
_ if editing_artboards => tl!("Artboard").to_string(),
|
||||
(None, _) => tl!("Document").to_string(),
|
||||
(_, n) if n > 1 => crate::i18n::tn(n as u64, "{n} Object", "{n} Objects"),
|
||||
(Some(n), _) if crate::panels::image_trace::is_trace(n) => tl!("Image Tracing").to_string(),
|
||||
|
|
@ -26,6 +29,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
};
|
||||
ui.label(egui::RichText::new(label).size(11.5).color(t.text_dim));
|
||||
ui.add_space(4.0);
|
||||
if editing_artboards {
|
||||
artboard_sections(app, ui);
|
||||
return;
|
||||
}
|
||||
if first.is_none() {
|
||||
document_sections(app, ui);
|
||||
return;
|
||||
|
|
@ -188,6 +195,48 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// Edit Artboards (the Artboard tool): the active artboard's name, position and size, New Artboard
|
||||
/// and Delete Artboard, and Exit back to the Selection tool (#530).
|
||||
fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let units = app.session.general_unit();
|
||||
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
|
||||
let i = super::artboards::selected(app, n);
|
||||
if let Some(ab) = app.session.active().and_then(|d| d.doc.artboards.get(i)).cloned() {
|
||||
// Its name as it is (names are never translated).
|
||||
ui.label(egui::RichText::new(&ab.name).size(13.0).color(Tokens::get(ui.ctx()).text));
|
||||
let fw = ((ui.available_width() - 50.0) / 2.0).clamp(60.0, 110.0);
|
||||
egui::Grid::new("ab-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
let r = ab.rect;
|
||||
for (row, [(l1, k1, v1), (l2, k2, v2)]) in
|
||||
[[("X:", "x", r.x0), ("W:", "width", r.width())], [("Y:", "y", r.y0), ("H:", "height", r.height())]].into_iter().enumerate()
|
||||
{
|
||||
for (label, key, v) in [(l1, k1, v1), (l2, k2, v2)] {
|
||||
dim_label(ui, label);
|
||||
if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v), units, fw) {
|
||||
app.run("artboard.setProps", json!({"index": i, key: v})).ok();
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
let w = (ui.available_width() - 6.0) / 2.0;
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::flat_button(ui, tl!("New Artboard"), w).clicked() && app.run("artboard.new", json!({})).is_ok() {
|
||||
super::artboards::select(app, n);
|
||||
}
|
||||
if ui.add_enabled_ui(n > 1, |ui| widgets::flat_button(ui, tl!("Delete Artboard"), w)).inner.clicked() {
|
||||
app.run("artboard.delete", json!({"index": i})).ok();
|
||||
}
|
||||
});
|
||||
divider(ui);
|
||||
section_header(ui, tl!("Quick Actions"));
|
||||
if widgets::flat_button(ui, tl!("Exit"), w).clicked() {
|
||||
app.select_tool("selection");
|
||||
}
|
||||
}
|
||||
|
||||
fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let units = app.session.general_unit();
|
||||
section_header(ui, tl!("Document"));
|
||||
|
|
@ -416,3 +465,58 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{Event, PointerButton, Pos2, Rect, vec2};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// One frame of the panel with `events` → the texts painted, with their rects.
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>) -> Vec<(String, Rect)> {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if let egui::Shape::Text(t) = &s.shape {
|
||||
Some((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, texts: &[(String, Rect)], label: &str) {
|
||||
let at = texts.iter().find(|(t, _)| t == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"));
|
||||
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(app, ctx, vec![Event::PointerMoved(at), b(true)]);
|
||||
frame(app, ctx, vec![b(false)]);
|
||||
}
|
||||
|
||||
/// #530: Edit Artboards shows the active artboard, with New Artboard, Delete Artboard and Exit.
|
||||
#[test]
|
||||
fn edit_artboards_adds_artboards_and_exits() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
click(&mut app, &ctx, &texts, "Edit Artboards");
|
||||
assert_eq!(app.session.tool_id(), "artboard");
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Artboard 1") && texts.iter().any(|(t, _)| t == "200 pt"), "{texts:?}");
|
||||
click(&mut app, &ctx, &texts, "New Artboard");
|
||||
let boards = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.len();
|
||||
assert_eq!(boards(&app), 2);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Artboard 2"), "the new artboard is the active one: {texts:?}");
|
||||
click(&mut app, &ctx, &texts, "Delete Artboard");
|
||||
assert_eq!(boards(&app), 1);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
click(&mut app, &ctx, &texts, "Exit");
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,3 +318,61 @@ fn the_search_field_stays_put_while_the_list_scrolls() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #563: a preview stays on the text while the menu stays on its family. The menu's caller passes
|
||||
/// the text's font, which the preview changed; the menu keeps the font from before the preview as
|
||||
/// the current one, so it doesn't take the previewed family for it and end the preview, then
|
||||
/// preview it again, every frame (the text flickered).
|
||||
#[test]
|
||||
fn a_preview_stays_put_while_the_menu_is_still() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
let font = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.first_style().font_family,
|
||||
_ => String::new(),
|
||||
};
|
||||
let revision = |app: &VectorcraftApp| app.session.active().unwrap().revision;
|
||||
// A panel's frame: the menu shows the text's font, and what it picks is applied.
|
||||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let input =
|
||||
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() };
|
||||
let current = font(app);
|
||||
let (mut at, mut picked) = (egui::Pos2::ZERO, None);
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
crate::font_menu::end_stale_preview(app, ui.ctx());
|
||||
at = ui.next_widget_position();
|
||||
picked = font_menu(ui, "test-steady", ¤t, 260.0, None, MenuLook::default());
|
||||
if let Some(p) = picked.clone() {
|
||||
crate::font_menu::apply(app, ui.ctx(), p);
|
||||
}
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
(at, picked)
|
||||
};
|
||||
let (at, _) = frame(&mut app, vec![]);
|
||||
let click = at + egui::vec2(40.0, 10.0);
|
||||
let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(click), button(true)]);
|
||||
frame(&mut app, vec![button(false)]);
|
||||
for _ in 0..5 {
|
||||
frame(&mut app, vec![]);
|
||||
}
|
||||
let (_, picked) = frame(&mut app, key(egui::Key::ArrowDown));
|
||||
assert!(matches!(picked, Some(FontPick::Preview(..))), "{picked:?}");
|
||||
let previewed = font(&app);
|
||||
assert_ne!(previewed, "Inter");
|
||||
let rev = revision(&app);
|
||||
for _ in 0..6 {
|
||||
let (_, picked) = frame(&mut app, vec![]);
|
||||
assert_eq!(picked, None, "nothing more to apply");
|
||||
}
|
||||
assert_eq!((font(&app), revision(&app)), (previewed, rev), "the preview stays, untouched");
|
||||
// Back on the text's own family, the preview ends.
|
||||
let (_, picked) = frame(&mut app, key(egui::Key::ArrowUp));
|
||||
assert_eq!(picked, Some(FontPick::EndPreview));
|
||||
assert_eq!(font(&app), "Inter");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2382,8 +2382,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, 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`,
|
||||
registered outside them, such as fonts installed as shortcuts, and in the desktop app and `vectorcraft-cli` (MCP
|
||||
included) 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue