Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c44572d272 | ||
|
|
1b43d8b8f9 | ||
|
|
2b321d240f | ||
|
|
2139c4d661 | ||
|
|
179a87d0a6 | ||
|
|
b832599a47 | ||
|
|
d7b4d85d3c | ||
|
|
af7a5239be | ||
|
|
c3279f1bcf | ||
|
|
4f07bd93b0 | ||
|
|
086d6287be | ||
|
|
0e6a47e72d | ||
|
|
f8004250fb | ||
|
|
03288f34dc | ||
|
|
aa4fa48751 | ||
|
|
165db95dd4 | ||
|
|
ac043129c8 | ||
|
|
8e753d746e | ||
|
|
b5759662c1 |
No files matched your search
Generated
+25
-25
@@ -4296,7 +4296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"block2 0.6.2",
|
||||
@@ -4320,7 +4320,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-brush"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"serde",
|
||||
@@ -4332,7 +4332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-cad"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4348,7 +4348,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-cli"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"vectorcraft-color",
|
||||
@@ -4364,7 +4364,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-color"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"moxcms",
|
||||
@@ -4376,7 +4376,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-doc"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"jpeg-encoder",
|
||||
@@ -4393,7 +4393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-effects"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"serde_json",
|
||||
@@ -4407,7 +4407,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-engine"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
@@ -4439,7 +4439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-eps"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
@@ -4457,7 +4457,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-format"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"proptest",
|
||||
@@ -4471,7 +4471,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-geom"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"proptest",
|
||||
@@ -4483,7 +4483,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-mcp"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4502,7 +4502,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-metafile"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4516,7 +4516,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-pathops"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"linesweeper",
|
||||
@@ -4528,7 +4528,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-pdf"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"flate2",
|
||||
@@ -4557,7 +4557,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-plugins"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4571,7 +4571,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-render"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"gif",
|
||||
@@ -4596,7 +4596,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-svg"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
@@ -4620,7 +4620,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-testkit"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4637,7 +4637,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-text"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"harfrust 0.13.3",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4655,7 +4655,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-tools"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4670,7 +4670,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-trace"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4683,7 +4683,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-ui-egui"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
@@ -4712,7 +4712,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-web"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5815,7 +5815,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/vectorcraft", "apps/vectorcraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
+6
-5
@@ -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 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 window (#493), panels, panel groups and the Tools panel undocking into flLine truncated
|
||||
_Last updated: 2026-10-09 (after 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 window (#493), panels, panel groups and the Tools panel undocking into flLine truncated
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -42,7 +42,7 @@ since 2026-10-01.
|
||||
| **Bundled content:** brush, symbol, style and swatch libraries | ~30% | Ours are original and generated in code, and far fewer than Illustrator ships; brush and symbol libraries are still missing |
|
||||
| **Performance** | unverified | Multithreaded, off-thread rendering and caches are in place. The last budget run (2026-10-01, loaded machine) measured 290 ms for a 50k-path fit against a 16 ms budget. Re-run `vectorcraft-cli perf` on an idle machine |
|
||||
| **Robustness** | good, new | ~2,840 tests, property tests, no panics in shipped code (lints, the `guard` safety net, import fuzzing), Data Recovery. Missing: a corpus of real-world files, Windows/Linux/browser QA |
|
||||
| **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work, and Finder opens every readable format with the macOS app (#295, #354); on macOS the menus are in the system menu bar, laid out as a Mac app's, with the in-window menus as a fallback (`--in-window-menus`; verified on Windows and by cross-compilation, not yet by hand on a Mac); hybrid-graphics laptops render on the power-saving GPU by default on Windows and macOS (#306), and Linux on the GPU the desktop runs on, since a Wayland compositor may refuse frames from the other one; a window that fails on its GPU while starting up starts again on the next one instead of crashing (#502; the dual-GPU Wayland case is verified by unit tests and cross-compilation, not on such hardware); web drops land in the document they were dropped on (#359); desktop drops carry no pointer position (winit 0.30), so a dropped document opens as a tab of its own and a picture or text is placed in the middle of the view, while the web places anything dropped on the canvas and opens what is dropped off it, as Illustrator does (#472); a web page that loses its WebGPU device or WebGL context restarts its graphics with the same documents (#369); on Linux/Wayland the clipboard reads the compositor's own (data-control) and copied image files paste, but files can't be dropped on the window under Wayland until eframe moves to winit 0.31 (#398; the Wayland side is verified by compilation only); on Linux the Paste menu checks the system clipboard on a background thread, so a clipboard owner that never answers no longer freezes the window, while Windows and macOS keep their quick in-line check (#510; the X11 hang verified by the contributor, here by headless tests); on KDE Plasma 6.3+ under Wayland, pens and pen displays reach the app only under XWayland (`WAYLAND_DISPLAY=`) or with KWin's `KWIN_WAYLAND_EMULATE_TABLET=1`, since winit 0.30 has no Wayland tablet input (#491, not verified on hardware); Windows/Linux packaging and accessibility are pending; an experimental, unsupported Windows 7 x64 OpenGL build exists (community-contributed, not verified by maintainers) |
|
||||
| **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work, and Finder opens every readable format with the macOS app (#295, #354); on macOS the menus are in the system menu bar, laid out as a Mac app's, with the in-window menus as a fallback (`--in-window-menus`; verified on Windows and by cross-compilation, not yet by hand on a Mac); hybrid-graphics laptops render on the power-saving GPU by default on Windows and macOS (#306), and Windows renders through DX12 before Vulkan (#545: Vulkan flickered black on an Intel GPU, DX12 and OpenGL didn't; verified by unit tests and the reporter's runs with `WGPU_BACKEND`), and Linux on the GPU the desktop runs on, since a Wayland compositor may refuse frames from the other one; a window that fails on its GPU while starting up starts again on the next one instead of crashing (#502; the dual-GPU Wayland case is verified by unit tests and cross-compilation, not on such hardware); web drops land in the document they were dropped on (#359); desktop drops carry no pointer position (winit 0.30), so a dropped document opens as a tab of its own and a picture or text is placed in the middle of the view, while the web places anything dropped on the canvas and opens what is dropped off it, as Illustrator does (#472); a web page that loses its WebGPU device or WebGL context restarts its graphics with the same documents (#369); on Linux/Wayland the clipboard reads the compositor's own (data-control) and copied image files paste, but files can't be dropped on the window under Wayland until eframe moves to winit 0.31 (#398; the Wayland side is verified by compilation only); on Linux the Paste menu checks the system clipboard on a background thread, so a clipboard owner that never answers no longer freezes the window, while Windows and macOS keep their quick in-line check (#510; the X11 hang verified by the contributor, here by headless tests); on KDE Plasma 6.3+ under Wayland, pens and pen displays reach the app only under XWayland (`WAYLAND_DISPLAY=`) or with KWin's `KWIN_WAYLAND_EMULATE_TABLET=1`, since winit 0.30 has no Wayland tablet input (#491, not verified on hardware); Windows/Linux packaging and accessibility are pending; an experimental, unsupported Windows 7 x64 OpenGL build exists (community-contributed, not verified by maintainers) |
|
||||
| **Agent automation** | beyond Illustrator | Every command, gesture and dialog is drivable over MCP, the CLI and the control channel |
|
||||
| **Scrubby numeric fields** | beyond Illustrator | As in Photoshop, dragging a numeric field's label (`W:`, `Opacity:`, the Character panel's icons) left or right steps its value live; one undo step per drag, Escape cancels (#400) |
|
||||
|
||||
@@ -112,8 +112,9 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- 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.
|
||||
- The Control bar's underlined Transform link opens the Transform panel in a popover under it (reference point, X/Y/W/H, Rotate and Shear with their icons and angle presets, flips, shape properties, Scale Corners and Scale Strokes & Effects), as its Stroke link does the Stroke panel; X/Y/W/H also show inline while the bar has room. The reference point locator is nine squares, the outer eight joined around the box.
|
||||
- Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields.
|
||||
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too. Not yet: Select All across threaded frames (one frame's text is selected).
|
||||
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected).
|
||||
- On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one.
|
||||
- **Tools:**
|
||||
- **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso. A Shift-drag marquee toggles what it reaches: objects with the Selection tool, anchors with Direct and Group Selection (#483); the Lasso adds with Shift and subtracts with Alt. Dragging a path segment with Direct Selection bends it when it's curved (its anchors stay) and moves its two anchors when it's straight, one undo step; the fill, Shift or Alt still pick the whole path (Alt-drag copies it).
|
||||
@@ -140,7 +141,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), Area Type Options (rows/columns/inset/first baseline), threaded text across any closed shapes, Fit Headline. Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458).
|
||||
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, Spanish (neutral international Spanish, for every `es-*` locale) and Italian (every `it-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); Spanish and Italian also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, Spanish and Italian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese, and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a wrapped line flush with its start, and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline or the em box top/right, centre or bottom/left (ICF top and bottom not yet); Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks.
|
||||
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own; filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points or an image over 64 megapixels is refused with advice rather than running out of memory, and a selection too large to outline shows its bounds (#525); with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), the Image Trace panel and Expand, plus Release in Properties; the View modes are not done), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror).
|
||||
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own; filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points or an image over 64 megapixels is refused with advice rather than running out of memory, and a selection too large to outline shows its bounds (#525); Object › Image Trace › Make and Make and Expand trace at once with the Default preset, #543; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), the Image Trace panel and Expand, plus Release in Properties; the View modes are not done), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror).
|
||||
- **Paint and appearance (M3):**
|
||||
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries and five gradient libraries in a library panel, saved and loaded as `.vcswatches`, `.gpl` or CSS. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.
|
||||
- **Colour:** a Color Picker (colour field, H/S/B/R/G/B channel slider, HSB/RGB/Lab/CMYK fields, hex, web-only snapping, out-of-gamut correction); the Color panel follows each colour's own model and Alt-click paints the other proxy; proxies look through groups and show "?" for mixed paint; Invert, Complement, Apply Last Color (`,`) and Apply Last Gradient (`.`) keep the colour model; every paint command feeds the recent colours. The Color Guide has 21 harmony rules, Steps and Variation options and Limit to Library. Adjust Color Balance (its Global mode shifts tints) and Saturate preview live; Edit Colors and Recolor also reach meshes, images and patterns. Recolor Artwork has colour reduction, five recolour methods, presets, Limit to Library, a colour-wheel Edit tab and Edit Color Group; a local Color Themes panel saves five-colour themes. Fill/Stroke chips open swatch and mixer popovers (the Properties panel has the Control bar's Fill and Stroke chips, Stroke link and weight spinner, with nothing selected too), panels have Illustrator's Window menu keys (Align Shift+F7, Transform Shift+F8, Pathfinder Cmd+Shift+F9, Layers F7, Character Cmd+T…; every panel can be given one in Edit › Keyboard Shortcuts), and Eyedropper Options pick up and apply chosen attributes (Alt and Shift+Alt, image pixels). Fills and strokes can overprint (Attributes panel, Overprint Black options), and CMYK documents blend in inks on screen, in PDF and when flattening. Document Color Mode converts the art, symbols, pattern tiles and swatches through the colour settings: Gray stays Gray, RGB greys separate into four-colour greys and a rich black as Illustrator does, or go on the black plate only with `grays: "black"` (also for `document.open {colorMode}`).
|
||||
@@ -159,7 +160,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export. EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole).
|
||||
- Place and the Links panel, Package, File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
|
||||
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped.
|
||||
- **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so any GPU that can show the window will do. Preferences › Performance › Graphics Processor (`gpuPreference`) is Automatic by default: the power-saving GPU on Windows and macOS (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306), and the GPU the desktop runs on under Linux, where KWin on an NVIDIA GPU refused frames from a Ryzen's integrated GPU and 0.5.0 crashed at startup (#502). Power Saving and High Performance pick a kind of GPU; `WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` and `WGPU_BACKEND` override them. The app picks among the adapters that can present to the window (native backends before OpenGL, hardware before software), and if the window fails on one while starting up it starts again without it instead of crashing; Help › About, `ui.inspect` and the log name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web). Pens and touch drive the tools as the mouse does, and a tap pressed and lifted within one frame clicks where it tapped (#491); pen pressure reaches the Liquify tools' Use Pressure Pen on Windows, while winit 0.30 reports none on macOS or Linux and egui none on the web. On Linux, see `docs/development.md` for pens under Wayland.
|
||||
- **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so any GPU that can show the window will do. Preferences › Performance › Graphics Processor (`gpuPreference`) is Automatic by default: the power-saving GPU on Windows and macOS (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306), and the GPU the desktop runs on under Linux, where KWin on an NVIDIA GPU refused frames from a Ryzen's integrated GPU and 0.5.0 crashed at startup (#502). Power Saving and High Performance pick a kind of GPU; `WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` and `WGPU_BACKEND` override them. The app picks among the adapters that can present to the window (native backends before OpenGL, hardware before software, and on Windows a GPU's DX12 adapter before its Vulkan one: Intel's Vulkan driver made the whole window flicker black, #545), and if the window fails on one while starting up it starts again without it instead of crashing; Help › About, `ui.inspect` and the log name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web). Pens and touch drive the tools as the mouse does, and a tap pressed and lifted within one frame clicks where it tapped (#491); pen pressure reaches the Liquify tools' Use Pressure Pen on Windows, while winit 0.30 reports none on macOS or Linux and egui none on the web. On Linux, see `docs/development.md` for pens under Wayland.
|
||||
- **Tests and robustness:** ~2,840 automated tests: model-based property tests, a junk-parameter sweep over every command, import fuzzing (SVG, PDF, libraries), golden renders, and MCP end-to-end tests over stdio. Shipped code never panics (workspace lints and a rollback safety net; see `docs/development.md`).
|
||||
|
||||
## Milestones and estimates
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
//! The canvas is rasterized on the CPU; the GPU only composites it and draws the UI, so any
|
||||
//! adapter that can present to the window will do. [`adapter_order`] ranks the adapters: those that
|
||||
//! can't present to the window's surface never, then the one `WGPU_ADAPTER_NAME` names, hardware
|
||||
//! before software, the native backends (Vulkan, Metal, DX12) before OpenGL, and the power
|
||||
//! preference ([`power_preference`]) among the rest. eframe is handed [`selector`], which takes
|
||||
//! the first of that order.
|
||||
//! before software, the native backends (Vulkan, Metal, DX12) before OpenGL, the power preference
|
||||
//! ([`power_preference`]) among the rest, and a GPU's DX12 adapter before its Vulkan one (#545).
|
||||
//! eframe is handed [`selector`], which takes the first of that order.
|
||||
//!
|
||||
//! An adapter can report that it presents to the window and still fail once it does: on a hybrid
|
||||
//! Linux desktop under Wayland, the compositor runs on one GPU and may refuse the frame buffers
|
||||
@@ -89,15 +89,17 @@ fn key(backend: Backend, vendor: u32, device: u32) -> String {
|
||||
|
||||
/// The order to try `candidates` in (their indices): only those that present to the window and
|
||||
/// aren't in `skip`; the one whose name contains `named` (any case) first; hardware before
|
||||
/// software; native backends before OpenGL; then by `power`, keeping the system's order among
|
||||
/// equals (all of it for [`PowerPreference::None`]).
|
||||
/// software; native backends before OpenGL; then by `power`; then DX12 before Vulkan, keeping the
|
||||
/// system's order among equals (all of it for [`PowerPreference::None`]). Windows lists each GPU
|
||||
/// under both, and Intel's Vulkan driver made the whole window flicker black where DX12 and OpenGL
|
||||
/// didn't (#545); elsewhere there is no DX12 adapter, so the order is unchanged.
|
||||
pub fn adapter_order(candidates: &[Candidate], power: PowerPreference, named: Option<&str>, skip: &[String]) -> Vec<usize> {
|
||||
let named = named.map(str::to_lowercase).filter(|n| !n.is_empty());
|
||||
let mut order: Vec<(usize, &Candidate)> = candidates.iter().enumerate().filter(|(_, c)| c.presents && !skip.contains(&c.key)).collect();
|
||||
// Stable, so equals keep the system's order.
|
||||
order.sort_by_key(|(_, c)| {
|
||||
let unnamed = named.as_ref().is_some_and(|n| !c.name.to_lowercase().contains(n));
|
||||
(unnamed, c.device_type == DeviceType::Cpu, c.backend == Backend::Gl, power_rank(c.device_type, power))
|
||||
(unnamed, c.device_type == DeviceType::Cpu, c.backend == Backend::Gl, power_rank(c.device_type, power), c.backend == Backend::Vulkan)
|
||||
});
|
||||
order.into_iter().map(|(i, _)| i).collect()
|
||||
}
|
||||
@@ -361,7 +363,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// A Windows machine lists every GPU twice (Vulkan and DX12): power saving still takes the
|
||||
/// integrated GPU first, as wgpu's own choice did (#306).
|
||||
/// integrated GPU first, as wgpu's own choice did (#306), each GPU through DX12 first (#545).
|
||||
#[test]
|
||||
fn hybrid_laptop_on_windows_renders_on_the_integrated_gpu_with_power_saving() {
|
||||
let c = vec![
|
||||
@@ -372,8 +374,25 @@ mod tests {
|
||||
gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu),
|
||||
];
|
||||
let order = adapter_order(&c, PowerPreference::LowPower, None, &[]);
|
||||
assert_eq!(order, [1, 3, 0, 2, 4]);
|
||||
assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, None, &[]), [0, 2, 1, 3, 4]);
|
||||
assert_eq!(order, [3, 1, 2, 0, 4]);
|
||||
assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, None, &[]), [2, 0, 3, 1, 4]);
|
||||
}
|
||||
|
||||
/// The machine of #545: one Intel GPU, whose Vulkan driver made the window flicker black.
|
||||
/// DX12 comes first whatever the preference; Vulkan stays a fallback, and `WGPU_ADAPTER_NAME`
|
||||
/// or a restart leaving DX12 out still reach it.
|
||||
#[test]
|
||||
fn a_windows_gpu_renders_through_dx12_before_vulkan() {
|
||||
let c = vec![
|
||||
gpu("Intel(R) Graphics", Backend::Vulkan, DeviceType::IntegratedGpu),
|
||||
gpu("Intel(R) Graphics", Backend::Dx12, DeviceType::IntegratedGpu),
|
||||
gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu),
|
||||
gpu("Intel(R) Graphics", Backend::Gl, DeviceType::Other),
|
||||
];
|
||||
for power in [PowerPreference::LowPower, PowerPreference::HighPerformance, PowerPreference::None] {
|
||||
assert_eq!(adapter_order(&c, power, None, &[]), [1, 0, 3, 2], "{power:?}");
|
||||
}
|
||||
assert_eq!(adapter_order(&c, PowerPreference::LowPower, None, &[c[1].key.clone()]), [0, 3, 2], "DX12 failed: Vulkan next");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -87,6 +87,26 @@ fn selected_part(n: &Node, keep: &HashSet<NodeId>) -> Option<Node> {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// `doc` with only the art over `rect` (an artboard): objects whose visual bounds miss it are left
|
||||
/// out, at any depth of layers and sublayers, and objects that reach it stay whole (the file's
|
||||
/// bounds clip them), as DXF's crop leaves them. A clipping layer keeps every object, its clipping
|
||||
/// path among them. An artboard's file then holds its own art, not a whole document of artboards.
|
||||
pub(super) fn art_over(doc: &Document, rect: vectorcraft_geom::Rect) -> Document {
|
||||
fn kept(n: &Arc<Node>, rect: vectorcraft_geom::Rect) -> Option<Arc<Node>> {
|
||||
if matches!(n.kind, NodeKind::Layer { clip: false, .. }) {
|
||||
let mut layer = Node::clone(n);
|
||||
let children = layer.children_mut()?;
|
||||
*children = children.iter().filter_map(|c| kept(c, rect)).collect();
|
||||
return Some(Arc::new(layer));
|
||||
}
|
||||
let over = |b: vectorcraft_geom::Rect| b.x0 <= rect.x1 && rect.x0 <= b.x1 && b.y0 <= rect.y1 && rect.y0 <= b.y1;
|
||||
n.visual_bounds().is_none_or(over).then(|| n.clone())
|
||||
}
|
||||
let mut d = doc.clone();
|
||||
d.layers = doc.layers.iter().filter_map(|l| kept(l, rect)).collect();
|
||||
d
|
||||
}
|
||||
|
||||
/// `p` without its artboard choice, also inside its SVG options (for documents made of one
|
||||
/// synthetic artboard, and for callers that pick the artboard themselves).
|
||||
pub(super) fn without_artboards(p: &Value) -> Value {
|
||||
|
||||
@@ -19,7 +19,7 @@ pub const OPTIONS: &[FormatOption] = &[
|
||||
name: "useArtboards",
|
||||
ty: "boolean",
|
||||
default: "true",
|
||||
description: "false: one SVG of the bounds of all art instead of artboards (several artboards write one file each)",
|
||||
description: "false: one SVG of the bounds of all art instead of artboards (several artboards write one file each; a named artboard's file holds only the art over it)",
|
||||
},
|
||||
FormatOption {
|
||||
name: "styling",
|
||||
@@ -125,8 +125,10 @@ pub fn options_map(p: &Value) -> Result<Map<String, Value>, String> {
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// The writer options and the artboards (`None`: the art bounds) an SVG export of `doc` covers.
|
||||
fn plan(doc: &Document, p: &Value) -> Result<(ExportOptions, Vec<Option<usize>>), String> {
|
||||
/// The writer options and the artboards (`None`: the art bounds) an SVG export of `doc` covers,
|
||||
/// and whether `p` named those artboards (else the first stands for the whole document, as Save
|
||||
/// writes it).
|
||||
fn plan(doc: &Document, p: &Value) -> Result<(ExportOptions, Vec<Option<usize>>, bool), String> {
|
||||
let mut m = options_map(p)?;
|
||||
// Fonts: unless chosen, Document Setup → Type → Export decides (appearance = outlines).
|
||||
if !m.contains_key("outlineText") && doc.setup.export_text == vectorcraft_doc::ExportText::Appearance {
|
||||
@@ -138,21 +140,24 @@ fn plan(doc: &Document, p: &Value) -> Result<(ExportOptions, Vec<Option<usize>>)
|
||||
opts.check().map_err(|e| format!("SVG options: {e}"))?;
|
||||
let n = doc.artboards.len();
|
||||
if n == 0 || boards.use_artboards == Some(false) {
|
||||
return Ok((opts, vec![None]));
|
||||
return Ok((opts, vec![None], false));
|
||||
}
|
||||
let picked = boards.pick.resolve(n)?.unwrap_or_else(|| vec![0]);
|
||||
Ok((opts, picked.into_iter().map(Some).collect()))
|
||||
let named = boards.pick.resolve(n)?;
|
||||
let chosen = named.is_some();
|
||||
Ok((opts, named.unwrap_or_else(|| vec![0]).into_iter().map(Some).collect(), chosen))
|
||||
}
|
||||
|
||||
/// Encode `doc` as SVG (or gzipped, SVGZ): one file per chosen artboard, with the images they
|
||||
/// link to and the writer's warnings.
|
||||
pub(super) fn encode(doc: &Document, p: &Value, compressed: bool) -> Result<Encoded, String> {
|
||||
let (opts, boards) = plan(doc, p)?;
|
||||
let (opts, boards, chosen) = plan(doc, p)?;
|
||||
// Preserve editing embeds the native document (once, shared by every file).
|
||||
let native = opts.preserve_editing.then(|| vectorcraft_format::save(doc, false));
|
||||
let mut enc = Encoded::default();
|
||||
for artboard in boards {
|
||||
let mut out = vectorcraft_svg::export_full(doc, &ExportOptions { artboard, ..opts.clone() }, native.as_deref());
|
||||
// A chosen artboard's file holds the art over it (#550).
|
||||
let over = artboard.filter(|_| chosen).and_then(|b| doc.artboards.get(b)).map(|a| super::export::art_over(doc, a.rect));
|
||||
let mut out = vectorcraft_svg::export_full(over.as_ref().unwrap_or(doc), &ExportOptions { artboard, ..opts.clone() }, native.as_deref());
|
||||
let bytes = out.take_bytes();
|
||||
for l in out.linked {
|
||||
if !enc.linked.iter().any(|e| e.name == l.name) {
|
||||
|
||||
@@ -49,3 +49,28 @@ fn profile_and_embedded_fonts_reach_the_writer() {
|
||||
assert!(s.execute("document.serialize", &json!({"format": "svg", "svg": {"profile": "svg2"}})).is_err(), "unknown profiles are refused");
|
||||
assert!(s.execute("document.serialize", &json!({"format": "svg", "encoding": "ebcdic"})).is_err(), "unknown encodings are refused");
|
||||
}
|
||||
|
||||
/// #550: an SVG of chosen artboards holds the art over each, not every artboard's art; with no
|
||||
/// artboard named (as Save writes it) the whole document's art is there.
|
||||
#[test]
|
||||
fn a_chosen_artboard_s_svg_holds_only_its_own_art() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100, "artboards": 3})).unwrap();
|
||||
let x = |s: &Session, b: usize| s.doc().unwrap().doc.artboards[b].rect.x0;
|
||||
let (first, third) = (x(&s, 0), x(&s, 2));
|
||||
s.execute("shape.rectangle", &json!({"x": first + 10.0, "y": 10, "width": 30, "height": 30})).unwrap();
|
||||
s.execute("shape.ellipse", &json!({"x": third + 10.0, "y": 10, "width": 30, "height": 30})).unwrap();
|
||||
// A rectangle has straight sides only; the ellipse is curves.
|
||||
let shapes = |svg: &str| (svg.matches("<path").count() + svg.matches("<rect").count(), svg.contains(" C") || svg.contains("<ellipse"));
|
||||
let text = |v: &serde_json::Value| v["text"].as_str().unwrap().to_string();
|
||||
let third_only = text(&s.execute("document.serialize", &json!({"format": "svg", "useArtboards": true, "range": "3"})).unwrap());
|
||||
assert_eq!(shapes(&third_only), (1, true), "{third_only}");
|
||||
let all = s.execute("document.serialize", &json!({"format": "svg", "useArtboards": true, "range": "all"})).unwrap();
|
||||
let files: Vec<String> = all["files"].as_array().unwrap().iter().map(text).collect();
|
||||
assert_eq!(files.iter().map(|f| shapes(f)).collect::<Vec<_>>(), [(1, false), (0, false), (1, true)]);
|
||||
// Export for Screens names one artboard at a time the same way.
|
||||
let screen = text(&s.execute("document.serialize", &json!({"format": "svg", "artboard": 0})).unwrap());
|
||||
assert_eq!(shapes(&screen), (1, false));
|
||||
let whole = text(&s.execute("document.serialize", &json!({"format": "svg"})).unwrap());
|
||||
assert_eq!(shapes(&whole).0, 2, "nothing named: the whole document's art");
|
||||
}
|
||||
@@ -174,6 +174,9 @@ pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorCon
|
||||
}
|
||||
}
|
||||
|
||||
/// The room the Control bar's inline X/Y/W/H fields need; with less, only the Transform link shows.
|
||||
const INLINE_TRANSFORM_WIDTH: f32 = 440.0;
|
||||
|
||||
/// The Control bar (Window → Control), context-sensitive like Illustrator's.
|
||||
pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -280,9 +283,13 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
// Transform fields.
|
||||
// The Transform link opens the whole Transform panel (reference point, rotate,
|
||||
// shear, options) in a popover; X/Y/W/H follow inline while the bar has room.
|
||||
crate::panels::transform::link(app, ui);
|
||||
// The bounding box, rotated with rotated objects: its centre and its own sides.
|
||||
if let Some(b) = app.selection_box() {
|
||||
if let Some(b) = app.selection_box()
|
||||
&& ui.available_width() >= INLINE_TRANSFORM_WIDTH
|
||||
{
|
||||
let c = b.center();
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
for (k, lbl, v) in [("x", "X:", c.x), ("y", "Y:", c.y), ("width", "W:", b.rect.width()), ("height", "H:", b.rect.height())] {
|
||||
|
||||
@@ -360,7 +360,7 @@ mod tests {
|
||||
let f = &mut app.ui.dialog.as_mut().unwrap().fields;
|
||||
f.insert("decimals".into(), json!(1));
|
||||
f.insert("allArtboards".into(), json!(false));
|
||||
f.insert("range".into(), json!("2"));
|
||||
f.insert("range".into(), json!("1"));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
let svg = text(&written, 0);
|
||||
|
||||
@@ -240,3 +240,29 @@ fn pdf_artboards_continue_in_the_save_pdf_dialog() {
|
||||
let pdf = vectorcraft_pdf::import(&written.borrow()[0].1).unwrap();
|
||||
assert_eq!(pdf.artboards.len(), 2, "one page per artboard in the range");
|
||||
}
|
||||
|
||||
/// #546, #550: Export As with Use Artboards and a range writes the chosen artboard alone in every
|
||||
/// format, each file holding that artboard's art (the ellipse is on artboard 1).
|
||||
#[test]
|
||||
fn export_as_writes_only_the_chosen_artboard_in_every_format() {
|
||||
for (format, ellipse) in [("svg", "<path"), ("dxf", "HATCH"), ("pdf", ""), ("png", "")] {
|
||||
for (range, on) in [("1", true), ("3", false)] {
|
||||
let (mut app, written) = app(3);
|
||||
app.run("file.exportAs", json!({"format": format})).unwrap();
|
||||
set(&mut app, "useArtboards", json!(true));
|
||||
set(&mut app, "all", json!(false));
|
||||
set(&mut app, "range", json!(range));
|
||||
confirm(&mut app).unwrap();
|
||||
// The format's own options, then the file.
|
||||
while app.ui.dialog.is_some() {
|
||||
frame(&mut app);
|
||||
confirm(&mut app).unwrap();
|
||||
}
|
||||
assert_eq!(names(&written), [format!("/out/Untitled-1.{format}")], "{format} {range}");
|
||||
if !ellipse.is_empty() {
|
||||
let text = String::from_utf8_lossy(&written.borrow()[0].1).into_owned();
|
||||
assert_eq!(text.contains(ellipse), on, "{format} {range}: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,12 +307,10 @@ fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, samp
|
||||
widgets::dim_label(ui, tl!("No matching fonts"));
|
||||
return None;
|
||||
}
|
||||
// The rows: only those in view are laid out (there are thousands of families). The list fills
|
||||
// what the popup shows below the filters (its clip: the popup doesn't grow past its own size,
|
||||
// and a taller list would scroll inside a scrolling popup), down to the window's bottom.
|
||||
const BOTTOM_GAP: f32 = 12.0;
|
||||
let bottom = ui.clip_rect().bottom().min(ui.ctx().content_rect().bottom() - BOTTOM_GAP);
|
||||
let room = (bottom - ui.next_widget_position().y).max(120.0);
|
||||
// The rows: only those in view are laid out (there are thousands of families). The list is
|
||||
// the menu's one scrolling part, under the search field and filters (#555): it fills the
|
||||
// menu's room (its max rect) below them.
|
||||
let room = (ui.max_rect().bottom() - ui.next_widget_position().y).clamp(120.0, 600.0);
|
||||
let mut area = egui::ScrollArea::vertical().max_height(room).min_scrolled_height(room.min(row_h * rows.len() as f32));
|
||||
// The highlight in view: centred as the menu opens, scrolled just enough as the keys move it.
|
||||
if let Some(top) = st.highlight.map(|i| i as f32 * row_h) {
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
Make Pixel Perfect Zarovnat na obrazové body
|
||||
Slice Řez
|
||||
Make Vytvořit
|
||||
Make and Expand Vytvořit a rozložit
|
||||
Release Uvolnit
|
||||
Create from Guides Vytvořit z vodítek
|
||||
Create from Selection Vytvořit z výběru
|
||||
@@ -199,8 +200,6 @@
|
||||
Merge Sloučit
|
||||
Gap Options… Možnosti mezer…
|
||||
Image Trace Vektorizace obrazu
|
||||
Make… Vytvořit…
|
||||
Make and Expand… Vytvořit a rozložit…
|
||||
Text Wrap Obtékání textu
|
||||
Text Wrap Options… Možnosti obtékání textu…
|
||||
Clipping Mask Ořezová maska
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1369,7 +1369,6 @@
|
||||
Make a theme on Create and save it. Crea un tema en Crear y guárdalo.
|
||||
Make all bleed settings the same Usar el mismo valor en todos los sangrados
|
||||
Make and Expand Crear y expandir
|
||||
Make and Expand… Crear y expandir…
|
||||
Make Clipping Mask Crear máscara de recorte
|
||||
Make Compound Shape Crear forma compuesta
|
||||
Make Envelope Crear envolvente
|
||||
@@ -1382,7 +1381,6 @@
|
||||
Make with Top Object Crear con objeto superior
|
||||
Make with Warp… Crear con deformación…
|
||||
Make/Release Clipping Mask Crear/soltar máscara de recorte
|
||||
Make… Crear…
|
||||
Manage Workspaces Administrar espacios de trabajo
|
||||
Manage Workspaces… Administrar espacios de trabajo…
|
||||
Manually Manualmente
|
||||
@@ -2546,6 +2544,7 @@
|
||||
Character: Carácter:
|
||||
Character options Opciones de carácter
|
||||
Stroke options Opciones de trazo
|
||||
Transform options Opciones de transformación
|
||||
Stroke Weight Grosor de trazo
|
||||
Stroke: Trazo:
|
||||
Stroke: click for swatches, Shift-click for the color mixer Trazo: clic para muestras, Shift+clic para el mezclador de colores
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1369,7 +1369,6 @@
|
||||
Make a theme on Create and save it. Crea un tema nella scheda Crea e salvalo.
|
||||
Make all bleed settings the same Usa lo stesso valore per tutte le abbondanze
|
||||
Make and Expand Crea ed espandi
|
||||
Make and Expand… Crea ed espandi…
|
||||
Make Clipping Mask Crea maschera di ritaglio
|
||||
Make Compound Shape Crea forma composta
|
||||
Make Envelope Crea involucro
|
||||
@@ -1382,7 +1381,6 @@
|
||||
Make with Top Object Crea con oggetto superiore
|
||||
Make with Warp… Crea con alterazione…
|
||||
Make/Release Clipping Mask Crea/rilascia maschera di ritaglio
|
||||
Make… Crea…
|
||||
Manage Workspaces Gestisci aree di lavoro
|
||||
Manage Workspaces… Gestisci aree di lavoro…
|
||||
Manually Manualmente
|
||||
@@ -2546,6 +2544,7 @@
|
||||
Character: Carattere:
|
||||
Character options Opzioni carattere
|
||||
Stroke options Opzioni traccia
|
||||
Transform options Opzioni di trasformazione
|
||||
Stroke Weight Spessore traccia
|
||||
Stroke: Traccia:
|
||||
Stroke: click for swatches, Shift-click for the color mixer Traccia: fai clic per i campioni, Shift+clic per il miscelatore colori
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -233,8 +233,6 @@
|
||||
Image Trace 画像トレース
|
||||
Image Tracing 画像トレースオブジェクト
|
||||
Tracing Presets トレースプリセット
|
||||
Make… 作成…
|
||||
Make and Expand… 作成して拡張…
|
||||
Text Wrap テキストの回り込み
|
||||
Text Wrap Options… テキストの回り込みオプション…
|
||||
Clipping Mask クリッピングマスク
|
||||
@@ -2352,6 +2350,7 @@
|
||||
Character: 文字:
|
||||
Character options 文字オプション
|
||||
Stroke options 線オプション
|
||||
Transform options 変形オプション
|
||||
Stroke: 線:
|
||||
Style スタイル
|
||||
Style Name: スタイル名:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1242,13 +1242,11 @@
|
||||
Make a theme on Create and save it. Crie um tema em Criar e salve-o.
|
||||
Make all bleed settings the same Tornar todas as configurações de sangria iguais
|
||||
Make and Expand Criar e Expandir
|
||||
Make and Expand… Criar e Expandir…
|
||||
Make the bleed values differ Fazer os valores de sangria diferirem
|
||||
Make with Mesh… Criar com Malha…
|
||||
Make with Top Object Criar com Objeto Superior
|
||||
Make with Warp… Criar com Distorção…
|
||||
Make/Release Clipping Mask Criar/Liberar Máscara de Recorte
|
||||
Make… Criar…
|
||||
Manage Workspaces Gerenciar Espaços de Trabalho
|
||||
Manage Workspaces… Gerenciar Espaços de Trabalho…
|
||||
Manually Manualmente
|
||||
@@ -2248,6 +2246,7 @@
|
||||
Character: Caractere:
|
||||
Character options Opções de caractere
|
||||
Stroke options Opções de traçado
|
||||
Transform options Opções de transformação
|
||||
Stroke: Traçado:
|
||||
Style Estilo
|
||||
Style Name: Nome do Estilo:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1351,7 +1351,6 @@
|
||||
Make a theme on Create and save it. 在“创建”中制作主题并存储。
|
||||
Make all bleed settings the same 使所有出血设置相同
|
||||
Make and Expand 创建并扩展
|
||||
Make and Expand… 制作并展开…
|
||||
Make Clipping Mask 制作剪切蒙版
|
||||
Make Compound Shape 创建复合形状
|
||||
Make Envelope 制作封套
|
||||
@@ -1364,7 +1363,6 @@
|
||||
Make with Top Object 用顶层对象创建
|
||||
Make with Warp… 用变形创建…
|
||||
Make/Release Clipping Mask 创建/释放剪切蒙版
|
||||
Make… 制作…
|
||||
Manage Workspaces 管理工作区
|
||||
Manage Workspaces… 管理工作区…
|
||||
Manually 手动
|
||||
@@ -2484,6 +2482,7 @@
|
||||
Character: 字符:
|
||||
Character options 字符选项
|
||||
Stroke options 描边选项
|
||||
Transform options 变换选项
|
||||
Stroke Weight 描边粗细
|
||||
Stroke: 描边:
|
||||
Stroke: click for swatches, Shift-click for the color mixer 描边:单击开启色板,按住 Shift 单击开启混色器
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1387,7 +1387,6 @@
|
||||
Make a theme on Create and save it. 按一下「建立」即可製作主題並儲存。
|
||||
Make all bleed settings the same 將所有出血設定設為相同
|
||||
Make and Expand 製作並展開
|
||||
Make and Expand… 製作並展開…
|
||||
Make Clipping Mask 製作剪裁遮色片
|
||||
Make Compound Shape 製作複合形狀
|
||||
Make Envelope 製作封套
|
||||
@@ -1400,7 +1399,6 @@
|
||||
Make with Top Object 以最上層物件製作
|
||||
Make with Warp… 以彎曲製作…
|
||||
Make/Release Clipping Mask 製作/釋放剪裁遮色片
|
||||
Make… 製作…
|
||||
Manage Workspaces 管理工作區
|
||||
Manage Workspaces… 管理工作區…
|
||||
Manually 手動
|
||||
@@ -2566,6 +2564,7 @@
|
||||
Character: 字元:
|
||||
Character options 字元選項
|
||||
Stroke options 筆畫選項
|
||||
Transform options 變形選項
|
||||
Stroke Weight 筆畫寬度
|
||||
Stroke: 筆畫:
|
||||
Stroke: click for swatches, Shift-click for the color mixer 筆畫:按一下開啟色票,按住 Shift 按一下開啟混色器
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
+69
-25
@@ -2157,8 +2157,11 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
sub(
|
||||
"Image Trace",
|
||||
vec![
|
||||
cp("Make…", "imageTrace.make", json!({"preset": "Black and White Logo"})),
|
||||
cp("Make and Expand…", "imageTrace.makeAndExpand", json!({"preset": "6 Colors"})),
|
||||
// Traced at once with the Default preset, as the Control bar's Image Trace
|
||||
// button does; other presets come from its arrow or the Image Trace panel
|
||||
// (#543: the "…" form asked for a preset's name in a text box).
|
||||
cp("Make", "imageTrace.make", json!({"preset": "Default"})),
|
||||
cp("Make and Expand", "imageTrace.makeAndExpand", json!({"preset": "Default"})),
|
||||
c("Release", "imageTrace.release"),
|
||||
c("Expand", "imageTrace.expand"),
|
||||
],
|
||||
@@ -2661,9 +2664,24 @@ pub fn ruler_menu_body(app: &VectorcraftApp, ui: &mut egui::Ui, clicked: &mut Op
|
||||
/// The in-window menu bar. Returns where its titles end (x): the bar itself takes the full width.
|
||||
pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ctx = ui.ctx().clone();
|
||||
// The press that opens a menu takes the keyboard from a focused text field: the field and its
|
||||
// selection are kept while the menus are open, so their Select All, Cut, Copy and Paste act on
|
||||
// its text (#516), the frame after the click that chose them (that click takes the keyboard
|
||||
// from it again).
|
||||
type Field = (egui::Id, egui::text_edit::TextEditState);
|
||||
let (field_key, chosen_key) = (egui::Id::new("menu-bar-field"), egui::Id::new("menu-bar-field-event"));
|
||||
if let Some(((field, state), e)) = ctx.data(|d| d.get_temp::<(Field, egui::Event)>(chosen_key)) {
|
||||
ctx.data_mut(|d| d.remove::<(Field, egui::Event)>(chosen_key));
|
||||
state.store(&ctx, field);
|
||||
to_field(&ctx, field, e);
|
||||
}
|
||||
if let Some(field) = focused_text_field(&ctx).and_then(|id| Some((id, egui::TextEdit::load_state(&ctx, id)?))) {
|
||||
ctx.data_mut(|d| d.insert_temp::<Field>(field_key, field));
|
||||
}
|
||||
let mut clicked: Option<(String, Value)> = None;
|
||||
let tree = menu_tree();
|
||||
let end = egui::MenuBar::new()
|
||||
let (end, open) = egui::MenuBar::new()
|
||||
.ui(ui, |ui| {
|
||||
let mut titles = Vec::with_capacity(tree.len());
|
||||
for (i, (title, items)) in tree.iter().enumerate() {
|
||||
@@ -2674,12 +2692,21 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 {
|
||||
};
|
||||
titles.push(ui.menu_button(text, |ui| menu_body(app, ui, items, &mut clicked)).response);
|
||||
}
|
||||
switch_on_hover(ui.ctx(), &titles);
|
||||
ui.cursor().min.x
|
||||
(ui.cursor().min.x, switch_on_hover(ui.ctx(), &titles))
|
||||
})
|
||||
.inner;
|
||||
let field = ctx.data(|d| d.get_temp::<Field>(field_key));
|
||||
if !open && focused_text_field(&ctx).is_none() {
|
||||
ctx.data_mut(|d| d.remove::<Field>(field_key));
|
||||
}
|
||||
if let Some((id, p)) = clicked {
|
||||
invoke(app, &id, p);
|
||||
match field.and_then(|f| Some((f, field_event(app, &id)?))) {
|
||||
Some(chosen) => {
|
||||
ctx.data_mut(|d| d.insert_temp(chosen_key, chosen));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
None => invoke(app, &id, p),
|
||||
}
|
||||
}
|
||||
end
|
||||
}
|
||||
@@ -2692,29 +2719,25 @@ fn pointer_reaches_title(ctx: &egui::Context, title: &egui::Response, p: egui::P
|
||||
}
|
||||
|
||||
/// Like a native menu bar: while one top-level menu is open, moving the pointer onto another
|
||||
/// title opens that menu instead (egui alone needs a click on each title).
|
||||
fn switch_on_hover(ctx: &egui::Context, titles: &[egui::Response]) {
|
||||
/// title opens that menu instead (egui alone needs a click on each title). Returns whether a menu
|
||||
/// is open.
|
||||
fn switch_on_hover(ctx: &egui::Context, titles: &[egui::Response]) -> bool {
|
||||
let ids: Vec<egui::Id> = titles.iter().map(egui::Popup::default_response_id).collect();
|
||||
let Some(open) = ids.iter().position(|id| egui::Popup::is_id_open(ctx, *id)) else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
// `Response::hovered` is false while a menu's popup is open, so hit-test the titles here.
|
||||
let Some(p) = ctx.pointer_hover_pos() else {
|
||||
return;
|
||||
};
|
||||
// Only a moving pointer switches: one resting on a title leaves the open menu alone.
|
||||
if ctx.input(|i| i.pointer.delta() == egui::Vec2::ZERO) {
|
||||
return;
|
||||
}
|
||||
let Some(i) = titles.iter().position(|title| pointer_reaches_title(ctx, title, p)) else {
|
||||
return;
|
||||
};
|
||||
if i != open
|
||||
let moving = ctx.input(|i| i.pointer.delta() != egui::Vec2::ZERO);
|
||||
if let Some(p) = ctx.pointer_hover_pos().filter(|_| moving)
|
||||
&& let Some(i) = titles.iter().position(|title| pointer_reaches_title(ctx, title, p))
|
||||
&& i != open
|
||||
&& let Some(id) = ids.get(i)
|
||||
{
|
||||
egui::Popup::open_id(ctx, *id);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A top-level menu's popup: as wide as its widest item (label plus shortcut), at least 230 pt;
|
||||
@@ -2859,13 +2882,21 @@ fn field_event(app: &mut VectorcraftApp, id: &str) -> Option<egui::Event> {
|
||||
/// Copy and Paste act on the field's text, as their keys do; everything else (and those commands
|
||||
/// with no field focused) goes to [`invoke`].
|
||||
pub fn invoke_from_system_menu(app: &mut VectorcraftApp, ctx: &egui::Context, id: &str, p: Value) {
|
||||
if ctx.text_edit_focused()
|
||||
&& let Some(e) = field_event(app, id)
|
||||
{
|
||||
ctx.input_mut(|i| i.events.push(e));
|
||||
return;
|
||||
match focused_text_field(ctx).and_then(|f| Some((f, field_event(app, id)?))) {
|
||||
Some((field, e)) => to_field(ctx, field, e),
|
||||
None => invoke(app, id, p),
|
||||
}
|
||||
invoke(app, id, p);
|
||||
}
|
||||
|
||||
/// The text field that has the keyboard, if any.
|
||||
fn focused_text_field(ctx: &egui::Context) -> Option<egui::Id> {
|
||||
ctx.memory(|m| m.focused()).filter(|&id| egui::TextEdit::load_state(ctx, id).is_some())
|
||||
}
|
||||
|
||||
/// Give text field `field` the keyboard (back) and event `e`, a [`field_event`], this frame.
|
||||
fn to_field(ctx: &egui::Context, field: egui::Id, e: egui::Event) {
|
||||
ctx.memory_mut(|m| m.request_focus(field));
|
||||
ctx.input_mut(|i| i.events.push(e));
|
||||
}
|
||||
|
||||
/// Invoke a menu/command id with UI side effects (dialogs for "…" commands that need input).
|
||||
@@ -3752,6 +3783,19 @@ mod tests {
|
||||
assert_eq!(pretty_shortcut(""), "");
|
||||
}
|
||||
|
||||
/// #543: Object › Image Trace › Make and Make and Expand trace with the Default preset at once,
|
||||
/// with no form asking for a preset's name.
|
||||
#[test]
|
||||
fn image_trace_make_traces_with_the_default_preset() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
for cmd in ["imageTrace.make", "imageTrace.makeAndExpand"] {
|
||||
let e = menu_entries(&app).into_iter().find(|e| e.command.as_deref() == Some(cmd)).unwrap();
|
||||
assert_eq!(e.path, ["Object", "Image Trace"]);
|
||||
assert_eq!(click_target(&e.label, cmd, &e.params), (cmd.to_string(), json!({"preset": "Default"})), "{}", e.label);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_appearance_is_an_object_menu_item_only() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -13,6 +13,8 @@ use crate::widgets::{self, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
pub const ANGLE_PRESETS: [f64; 9] = [-180.0, -135.0, -90.0, -45.0, 0.0, 45.0, 90.0, 135.0, 180.0];
|
||||
/// The shear angle's presets: steep enough to be useful, short of the degenerate ±90°.
|
||||
pub const SHEAR_PRESETS: [f64; 9] = [-60.0, -45.0, -30.0, -15.0, 0.0, 15.0, 30.0, 45.0, 60.0];
|
||||
|
||||
/// Proportional size: the other dimension when one changes with the link on.
|
||||
pub fn constrained(w: f64, h: f64, new_w: Option<f64>, new_h: Option<f64>) -> (f64, f64) {
|
||||
@@ -151,8 +153,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.add_space(4.0);
|
||||
let icon = icons::icon(ui, "dc-shear", 16.0, t.icon).on_hover_text(tl!("Shear"));
|
||||
crate::scrub::note_label(ui, icon.rect);
|
||||
if let Some(a) = widgets::plain_field(ui, "xfp-shear", 0.0, "°", 1, 56.0)
|
||||
// Shear is relative: the field rests at 0° and a new value shears by it.
|
||||
if let Some(a) = widgets::spin_plain(ui, "xfp-shear", 0.0, "°", 2, 96.0, 15.0, -89.0, &SHEAR_PRESETS)
|
||||
&& a != 0.0
|
||||
&& a.abs() < 90.0
|
||||
{
|
||||
app.run("object.shear", json!({"angle": a, "axis": "horizontal", "origin": origin})).ok();
|
||||
}
|
||||
@@ -206,6 +210,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Control bar's underlined "Transform" link: a click toggles the Transform panel in a
|
||||
/// popover under it, as the Stroke link does with the Stroke panel.
|
||||
pub fn link(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let resp = ui.link(egui::RichText::new(tl!("Transform")).size(12.0).color(t.text).underline()).on_hover_text(tl!("Transform options"));
|
||||
widgets::popover(&resp, resp.clicked(), |ui| {
|
||||
ui.set_width(300.0);
|
||||
show(app, ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// The link between W and H (Transform panel, Properties panel, Control bar): one toggle, the
|
||||
/// `constrainProportions` preference, which the size fields pass on as `proportional`.
|
||||
pub fn constrain_link(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
@@ -270,3 +270,51 @@ fn menus_that_only_pick_a_font_leave_the_document_alone() {
|
||||
assert_eq!(picked(&mut app, Some(FontPick::Favorite("inter".into()))), None);
|
||||
assert!(app.ui.favorite_fonts.is_empty());
|
||||
}
|
||||
|
||||
/// #555: the menu's search field and filters stay at its top while the wheel scrolls the families,
|
||||
/// past the list's end too: the list is the menu's one scrolling part.
|
||||
#[test]
|
||||
fn the_search_field_stays_put_while_the_list_scrolls() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let frame = |events: Vec<egui::Event>| {
|
||||
let input =
|
||||
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 500.0))), ..Default::default() };
|
||||
let mut at = egui::Pos2::ZERO;
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
// Low in the window, as in a panel at the bottom of the dock: the menu opens upwards.
|
||||
ui.add_space(390.0);
|
||||
at = ui.next_widget_position();
|
||||
font_menu(ui, "test-scroll", "Inter", 260.0, None, MenuLook::default());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let search = out.shapes.iter().find_map(|s| match &s.shape {
|
||||
egui::Shape::Text(t) if t.galley.text() == "Search" => Some(t.pos),
|
||||
_ => None,
|
||||
});
|
||||
(at, search)
|
||||
};
|
||||
let (at, _) = frame(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(vec![egui::Event::PointerMoved(click), button(true)]);
|
||||
frame(vec![button(false)]);
|
||||
let mut field = None;
|
||||
for _ in 0..5 {
|
||||
field = frame(vec![]).1;
|
||||
}
|
||||
let field = field.expect("the search field shows");
|
||||
let over_list = egui::pos2(field.x + 40.0, field.y + 120.0);
|
||||
for dy in [-400.0, -4000.0, -4000.0] {
|
||||
let wheel = egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::vec2(0.0, dy),
|
||||
modifiers: Default::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
};
|
||||
frame(vec![egui::Event::PointerMoved(over_list), wheel]);
|
||||
for _ in 0..10 {
|
||||
assert_eq!(frame(vec![]).1, Some(field), "the field stays where it was");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,3 +242,31 @@ fn the_window_menu_panel_keys() {
|
||||
app.ui.shortcut_overrides.clear();
|
||||
shortcut_editor::sync(&app.ui);
|
||||
}
|
||||
|
||||
/// Where `shapes` paint the text `s`.
|
||||
fn text_rect(shapes: &[Shape], s: &str) -> Option<Rect> {
|
||||
shapes.iter().find_map(|sh| match sh {
|
||||
Shape::Text(t) if t.galley.text() == s => Some(t.visual_bounding_rect()),
|
||||
Shape::Vec(v) => text_rect(v, s),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_transform_link_opens_the_transform_panel() {
|
||||
let mut app = app();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar);
|
||||
let shapes = frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar);
|
||||
let link = text_rect(&shapes, "Transform").expect("the Control bar shows the Transform link");
|
||||
assert!(text_rect(&shapes, "Scale Corners").is_none());
|
||||
click(&mut app, &ctx, link.center(), Modifiers::NONE, control_bar);
|
||||
let shapes = frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar);
|
||||
for s in ["Scale Corners", "Scale Strokes & Effects", "0°"] {
|
||||
assert!(text_rect(&shapes, s).is_some(), "the popover shows {s}");
|
||||
}
|
||||
// A click outside closes it.
|
||||
click(&mut app, &ctx, Pos2::new(1300.0, 900.0), Modifiers::NONE, control_bar);
|
||||
assert!(text_rect(&frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar), "Scale Corners").is_none());
|
||||
}
|
||||
@@ -171,3 +171,94 @@ fn cmd_a_on_the_canvas_selects_all_objects() {
|
||||
a.menu("select.all");
|
||||
assert_eq!(a.selected(), 2, "so does the menu's Select All");
|
||||
}
|
||||
|
||||
/// The copied texts of a frame's output.
|
||||
fn copied(out: &egui::FullOutput) -> Vec<String> {
|
||||
out.platform_output.commands.iter().filter_map(|c| if let egui::OutputCommand::CopyText(t) = c { Some(t.clone()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// #516: Ctrl/Cmd+C, X and V in a focused field (the window's own clipboard keys, which egui sends
|
||||
/// as Copy, Cut and Paste events) edit the field's text, never the art.
|
||||
#[test]
|
||||
fn the_clipboard_keys_in_a_focused_field_edit_its_text() {
|
||||
let mut a = App::new();
|
||||
a.app.run("window.panel", json!({"panel": "color"})).unwrap();
|
||||
for _ in 0..3 {
|
||||
a.frame(vec![]);
|
||||
}
|
||||
for text in ["37 pt", "FFFFFF"] {
|
||||
let (id, rect) = a.field(text);
|
||||
a.frame(vec![Event::PointerMoved(rect.center()), click(rect.center(), true)]);
|
||||
a.frame(vec![click(rect.center(), false)]);
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
assert!(a.ctx.memory(|m| m.has_focus(id)), "{text}: the field has the keyboard");
|
||||
let out = a.frame(vec![Event::Copy]);
|
||||
assert_eq!(copied(&out), [text], "Copy copies the field's text");
|
||||
assert!(a.app.session.clipboard.is_empty(), "{text}: no art copied");
|
||||
let out = a.frame(vec![Event::Cut]);
|
||||
assert_eq!(copied(&out), [text], "Cut cuts the field's text");
|
||||
a.frame(vec![Event::Paste("12".into())]);
|
||||
assert_eq!(a.ctx.data(|d| d.get_temp::<String>(id)).as_deref(), Some("12"), "Paste pastes into the field");
|
||||
assert_eq!(a.selected(), 1, "{text}: the art stays");
|
||||
a.frame(vec![press(Key::Escape, Modifiers::NONE)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The centre of the text `label` painted last (in the topmost layer) in a frame's output; with
|
||||
/// `title`, the first one painted (the menu bar's).
|
||||
fn text_at(out: &egui::FullOutput, label: &str, title: bool) -> Pos2 {
|
||||
let mut shapes: Box<dyn Iterator<Item = _>> = if title { Box::new(out.shapes.iter()) } else { Box::new(out.shapes.iter().rev()) };
|
||||
shapes
|
||||
.find_map(|s| match &s.shape {
|
||||
egui::Shape::Text(t) if t.galley.text().trim() == label => Some(Rect::from_min_size(t.pos, t.galley.size()).center()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| panic!("no {label:?} painted"))
|
||||
}
|
||||
|
||||
/// A click on `at` → the frame after it.
|
||||
fn click_at(a: &mut App, at: Pos2) -> egui::FullOutput {
|
||||
a.frame(vec![Event::PointerMoved(at), click(at, true)]);
|
||||
a.frame(vec![click(at, false)]);
|
||||
a.frame(vec![])
|
||||
}
|
||||
|
||||
/// The in-window menus' Select › All, Edit › Copy and Edit › Paste act on the text field that had the keyboard
|
||||
/// when the menu was opened (the click on the menu took it), and give it the keyboard back.
|
||||
#[test]
|
||||
fn the_in_window_edit_menu_acts_on_the_field_it_was_opened_from() {
|
||||
let mut a = App::new();
|
||||
// Text on the system clipboard.
|
||||
a.app.services.clipboard_read = Some(Box::new(|| Some("42 pt".into())));
|
||||
let (id, rect) = a.field("37 pt");
|
||||
a.frame(vec![Event::PointerMoved(rect.center()), click(rect.center(), true)]);
|
||||
a.frame(vec![click(rect.center(), false)]);
|
||||
let out = a.frame(vec![press(Key::End, Modifiers::NONE)]);
|
||||
let (edit, select) = (text_at(&out, "Edit", true), text_at(&out, "Select", true));
|
||||
let menu = |a: &mut App, title: Pos2, item: &str| {
|
||||
let out = click_at(a, title);
|
||||
assert!(!a.ctx.text_edit_focused(), "the menu took the keyboard");
|
||||
click_at(a, text_at(&out, item, false))
|
||||
};
|
||||
menu(&mut a, select, "All");
|
||||
assert!(a.ctx.memory(|m| m.has_focus(id)), "the field has the keyboard back");
|
||||
assert_eq!(a.field_selection(id), Some((0, 5)), "Select All selected the field's text");
|
||||
assert_eq!(a.selected(), 1, "not the art");
|
||||
a.frame(vec![press(Key::End, Modifiers::NONE)]);
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
a.frame(vec![]);
|
||||
let out = menu(&mut a, edit, "Copy");
|
||||
assert_eq!(copied(&out), ["37 pt"], "Copy copied the field's text");
|
||||
assert!(a.app.session.clipboard.is_empty(), "no art copied");
|
||||
// Art on the app's clipboard (Paste is enabled): the field still takes the text.
|
||||
a.app.run("edit.copy", json!({})).unwrap();
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
menu(&mut a, edit, "Paste");
|
||||
assert_eq!(a.ctx.data(|d| d.get_temp::<String>(id)).as_deref(), Some("42 pt"), "Paste pasted into the field");
|
||||
assert_eq!(a.selected(), 1, "nothing pasted on the canvas");
|
||||
// With no field focused, the menu's Select All selects the art.
|
||||
a.frame(vec![press(Key::Escape, Modifiers::NONE)]);
|
||||
a.frame(vec![]);
|
||||
menu(&mut a, select, "All");
|
||||
assert_eq!(a.selected(), 2);
|
||||
}
|
||||
@@ -655,11 +655,13 @@ pub(crate) fn combo<R>(
|
||||
.stroke(Stroke::new(1.0, t.input_border))
|
||||
.corner_radius(CornerRadius::same(3))
|
||||
.show(ui, |ui| {
|
||||
if searchable {
|
||||
return searchable_combo(ui, ui.id().with(id), current, width - 4.0, list);
|
||||
}
|
||||
egui::ComboBox::from_id_salt(ui.id().with(id))
|
||||
.selected_text(egui::RichText::new(current).size(12.0))
|
||||
.width(width - 4.0)
|
||||
.close_behavior(if searchable { egui::PopupCloseBehavior::CloseOnClickOutside } else { egui::PopupCloseBehavior::CloseOnClick })
|
||||
.height(if searchable { f32::INFINITY } else { ui.spacing().combo_height })
|
||||
.height(ui.spacing().combo_height)
|
||||
.show_ui(ui, list)
|
||||
.inner
|
||||
.flatten()
|
||||
@@ -667,6 +669,58 @@ pub(crate) fn combo<R>(
|
||||
.inner
|
||||
}
|
||||
|
||||
/// A dropdown whose list keeps its own search field and scrolling rows (the font menu): drawn as
|
||||
/// egui's combo box, but in a popup without the scroll area egui puts around a combo's list, so
|
||||
/// the field stays at the top and only the rows scroll (#555). A click outside closes it.
|
||||
fn searchable_combo<R>(ui: &mut Ui, id: egui::Id, current: &str, width: f32, list: impl FnOnce(&mut Ui) -> Option<R>) -> Option<R> {
|
||||
let popup = id.with("popup");
|
||||
let open = egui::Popup::is_id_open(ui.ctx(), popup);
|
||||
let pad = ui.spacing().button_padding;
|
||||
let icon = Vec2::splat(ui.spacing().icon_width);
|
||||
let galley = egui::WidgetText::from(egui::RichText::new(current).size(12.0)).into_galley(
|
||||
ui,
|
||||
Some(egui::TextWrapMode::Truncate),
|
||||
width - 2.0 * pad.x - ui.spacing().icon_spacing - icon.x,
|
||||
egui::TextStyle::Button,
|
||||
);
|
||||
let height = (galley.size().y.max(icon.y) + 2.0 * pad.y).max(ui.spacing().interact_size.y);
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(width, height), Sense::click());
|
||||
if ui.is_rect_visible(rect) {
|
||||
let v = if open { ui.visuals().widgets.open } else { *ui.style().interact(&resp) };
|
||||
ui.painter().rect(rect.expand(v.expansion), v.corner_radius, v.weak_bg_fill, v.bg_stroke, StrokeKind::Inside);
|
||||
let inner = rect.shrink2(pad);
|
||||
ui.painter().galley(egui::Align2::LEFT_CENTER.align_size_within_rect(galley.size(), inner).min, galley, v.text_color());
|
||||
// egui's combo arrow: a downward triangle.
|
||||
let arrow =
|
||||
Rect::from_center_size(egui::Align2::RIGHT_CENTER.align_size_within_rect(icon, inner).center(), vec2(icon.x * 0.7, icon.y * 0.45));
|
||||
ui.painter().add(egui::Shape::convex_polygon(
|
||||
vec![arrow.left_top(), arrow.right_top(), arrow.center_bottom()],
|
||||
v.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
// It opens below the button, or above it when there is more room there, and the list gets
|
||||
// the room left (the popup's frame aside).
|
||||
const GAP: f32 = 12.0;
|
||||
let screen = ui.ctx().content_rect();
|
||||
let frame = ui.spacing().menu_margin.sum().y + GAP;
|
||||
let (below, above) = (screen.bottom() - rect.bottom() - frame, rect.top() - screen.top() - frame);
|
||||
let (align, room) = if below >= above { (egui::RectAlign::BOTTOM_START, below) } else { (egui::RectAlign::TOP_START, above) };
|
||||
egui::Popup::menu(&resp)
|
||||
.id(popup)
|
||||
.width(rect.width())
|
||||
.align(align)
|
||||
.align_alternatives(&[])
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||
.show(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.set_max_height(room);
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
list(ui)
|
||||
})
|
||||
.and_then(|r| r.inner)
|
||||
}
|
||||
|
||||
/// A popover anchored to `resp`, opened and closed by `toggle` (usually `resp.clicked()`), and
|
||||
/// closed by Escape, a click outside it and outside any popup it opened, or a frame in which its
|
||||
/// anchor isn't shown. Use it instead of `egui::Popup::menu` when the popover holds dropdowns or
|
||||
@@ -806,18 +860,23 @@ pub fn opacity_blend(
|
||||
edit
|
||||
}
|
||||
|
||||
/// The 3×3 reference point locator. Returns a new index when clicked.
|
||||
/// The 3×3 reference point locator: nine squares, the eight outer ones joined by a line around
|
||||
/// the box (the centre one stands alone), the current one filled. Returns a new index when clicked.
|
||||
pub fn reference_point(ui: &mut Ui, current: usize) -> Option<usize> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let size = 22.0;
|
||||
// 5 pt squares with 4 pt gaps; the origin is snapped to whole points so the 1 pt lines through
|
||||
// the squares' centres stay crisp.
|
||||
let (sq, step) = (5.0, 9.0);
|
||||
let size = 2.0 * step + sq;
|
||||
let (rect, _) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover());
|
||||
let o = rect.min.round();
|
||||
let centre = |i: usize| o + Vec2::new(sq / 2.0 + (i % 3) as f32 * step, sq / 2.0 + (i / 3) as f32 * step);
|
||||
let line = Stroke::new(1.0, t.text_dim);
|
||||
ui.painter().rect_stroke(Rect::from_two_pos(centre(0), centre(8)), 0.0, line, StrokeKind::Middle);
|
||||
let mut out = None;
|
||||
let step = size / 2.0 - 2.5;
|
||||
ui.painter().rect_stroke(rect.shrink(4.5), 0.0, Stroke::new(1.0, t.text_dim), StrokeKind::Middle);
|
||||
for i in 0..9 {
|
||||
let c = Pos2::new(rect.left() + 3.0 + (i % 3) as f32 * step + 1.5, rect.top() + 3.0 + (i / 3) as f32 * step + 1.5);
|
||||
let r = Rect::from_center_size(c, Vec2::splat(5.0));
|
||||
let resp = ui.interact(r.expand(1.5), ui.id().with(("refpt", i)), Sense::click());
|
||||
let r = Rect::from_center_size(centre(i), Vec2::splat(sq));
|
||||
let resp = ui.interact(r.expand(2.0), ui.id().with(("refpt", i)), Sense::click());
|
||||
if resp.clicked() {
|
||||
out = Some(i);
|
||||
}
|
||||
@@ -825,7 +884,7 @@ pub fn reference_point(ui: &mut Ui, current: usize) -> Option<usize> {
|
||||
ui.painter().rect_filled(r, 0.0, t.text);
|
||||
} else {
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.text_dim), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, if resp.hovered() { t.text } else { t.text_dim }), StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
out
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop:
|
||||
|
||||
The canvas is rasterized on the CPU (`vectorcraft-render`, vello_cpu); the GPU (wgpu, through eframe) only composites the canvas texture and draws the UI, so any GPU that can show the window will do. The desktop app picks the window's adapter itself (`apps/vectorcraft/src/gpu.rs`, eframe's `native_adapter_selector`) and logs every adapter it found and the one it uses.
|
||||
|
||||
- **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; then the power preference, keeping the system's order among equals.
|
||||
- **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; then the power preference; then, on Windows, where each GPU is listed under both, a GPU's DX12 adapter before its Vulkan one (Intel's Vulkan driver made the whole window flicker black, #545), keeping the system's order among equals. `WGPU_BACKEND=vulkan` still picks Vulkan.
|
||||
- **Preferences › Performance › Graphics Processor** (`gpuPreference`): `automatic` (the default), `lowPower` (Power Saving, the integrated GPU) or `highPerformance` (the discrete GPU). The adapter is chosen when the window opens, so a change applies after a restart.
|
||||
- **Automatic on Windows and macOS** is power saving: the system shows frames from any GPU, and presenting frames rendered on a discrete GPU through the integrated one made the window flicker on some hybrid laptops (#306).
|
||||
- **Automatic on Linux and the BSDs** keeps the system's order: Mesa's Vulkan device-select layer puts the GPU the desktop runs on first (the integrated one on hybrid laptops; `DRI_PRIME` and `MESA_VK_DEVICE_SELECT` steer it). A Wayland compositor may not accept frames from another GPU: on a desktop whose compositor ran on an NVIDIA GPU, rendering on the Ryzen's integrated GPU made KWin end the window's connection ("importing the supplied dmabufs failed") and 0.5.0 crashed at startup (#502).
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ colour group (one undo step).
|
||||
SVG Options: `export` to SVG takes them in `options`, flat or as `{"svg": {…}}`: `styling` (`presentation`,
|
||||
`style`, `entities`, `css`), `outlineText`, `images` (`embed`, or `link`: embedded images are written next to the
|
||||
SVG, or returned as `linked`), `objectIds` (`layerNames`, `minimal`, `unique`), `decimals` (1–7), `minify`,
|
||||
`responsive`, `useArtboards`, `range: "all"` (one SVG per artboard, listed in `files`), `preserveEditing` (the SVG
|
||||
`responsive`, `useArtboards`, `range: "all"` (one SVG per artboard, listed in `files`, each holding only the art over its artboard), `preserveEditing` (the SVG
|
||||
reopens as the full document), `metadata` and `fewerTspans` (one `<tspan>` per line of type). Unknown keys inside `svg` are rejected; `run_command document.formats`
|
||||
lists every option with its default. `encoding` is `utf8`, `utf16` (big-endian after a byte order mark) or `latin1`
|
||||
(ISO 8859-1, other characters as `&#x…;` references); `document.serialize` still answers `text`, plus `dataBase64`
|
||||
|
||||
Reference in new issue
Block a user