Control bar: image controls (Image Trace, Embed, Crop, Mask) when an image is selected

Discord feedback: Image Trace and the other image options were hidden when an image was
selected; copy, paste, Image Trace should work from the Control bar as in Illustrator.

- Control bar, one image selected: Linked File/Embedded, file name, colour mode and PPI, then
  Embed (+ Edit Original) or Unembed…, an Image Trace split button (click: Default preset;
  arrow: trace with any preset), Mask and Crop Image, in place of Fill and Stroke.
- Control bar, one Image Trace object selected: "Image Tracing", its Preset (choosing another
  traces it again), the Image Trace panel and Expand.
- Properties panel: Image Trace (split), Crop Image and Mask in Quick Actions for an image; an
  Image Trace section (Preset, Expand, Release) for an Image Trace object. Embed/Unembed buttons
  are shared with the Control bar (place::link_buttons).
- New engine command object.maskImage ("Mask"): clips the selected image with a clipping path
  on its outline (rotated with it) and selects that path so its handles crop; one undo step.
- widgets::split_button (flat button with a menu arrow).
- Translations for the new strings (es, ja, zh-hans, zh-hant; "Mask" also pt-br, cs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-08 17:15:48 +07:00
1 parent 87ba4f6454
commit d6f8cc879f
16 files changed
+351 -55

No files matched your search

+3 -3
View File
@@ -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-08 (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); see [Honest assessment](#honest-assessment-2026-10-05))._
_Last updated: 2026-10-08 (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); see [Honest assessment](#honest-assessment-2026-10-05))._
## Where we are
@@ -136,7 +136,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 and Spanish (neutral international Spanish, for every `es-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); Spanish also translates 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 and Spanish 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; 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), 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; 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}`).
@@ -207,7 +207,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
| Type core | 9 | 79% | Middle Eastern features beyond bidi layout and Paragraph Direction (Character Direction, digit types, kashidas, the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); composer/hyphenation options, Optical Margin Alignment, hidden characters, Transform Again after a type-area resize (it scales the type), partial selection of type-area anchors by marquee | 15–20 |
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), hanging punctuation, kinsoku sets and the Mojikumi Settings dialog —, proportional vertical metrics, manual tate-chu-yoko, Character Alignment on the ICF top and bottom), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support | 20–28 |
| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | symbol libraries (original), dynamic symbols, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done) | 5–8 |
| Image Trace, graphs, image tools | 3 | 70% | graph Design/Column/Marker, Create Object Mosaic, Crop Image polish (Vector Halftone is done) | 6–10 |
| Image Trace, graphs, image tools | 3 | 70% | graph Design/Column/Marker, Create Object Mosaic, Crop Image polish (an on-canvas crop box), Image Trace View modes (Outlines, Source Image) (Vector Halftone is done) | 6–10 |
| Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, Collect for Export, row context menus, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers, drag-scrolling a long panel while dragging rows; artboard presets/rearrange polish (Document Setup, New Document and the rest of the Layers panel are done) | 4–7 |
| View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 |
| Guides, grids, smart guides, snapping, rulers | 3 | 82% | global/video rulers, smart-guide preference depth, Alt swapping a ruler guide's orientation, ruler guides selected together with art (marquee, Shift-click), ruler guides as Layers rows with their own lock and hide (#451: Lock Guides locks them all; Make Guides paths are Layers rows already) (dragging existing guides is done, #414; new guides snapping to side midpoints and artboards, and artboard guides, #451) | 3–5 |
+40 -1
View File
@@ -1,5 +1,5 @@
//! Object menu long tail: Lock/Hide All Artwork Above & Other Layers, Transform Each, Reset
//! Bounding Box, Rasterize, Crop Image, Create Trim Marks, Convert to Shape and
//! Bounding Box, Rasterize, Crop Image, Mask (an image), Create Trim Marks, Convert to Shape and
//! Artboards → Convert / Rearrange. (Live blends are in `live.rs`.)
use std::sync::Arc;
@@ -115,6 +115,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_image,
crop_image
),
cmd!(
"object.maskImage",
"Mask",
[],
None,
"{} clip the selected image with a rectangle around it (its own outline, rotated with it) and select that clipping path, whose handles then crop it; one undo step → {id: the clip group, path: the clipping path}",
has_image,
mask_image
),
cmd!(
"object.createTrimMarks",
"Create Trim Marks",
@@ -585,6 +594,36 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "id": id.0, "width": w, "height": h }))
}
/// The Control bar's Mask for an image: a clip group of the image and a clipping path on its
/// outline, with that path selected so dragging its handles crops the image.
fn mask_image(s: &mut Session, _: &Value) -> Result<Value> {
let st = s.doc()?;
let (id, outline) = st
.selection
.objects
.iter()
.find_map(|id| match st.doc.node(*id).map(|n| &n.kind) {
Some(NodeKind::Image(im)) => {
Some((*id, vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, im.width as f64, im.height as f64)).transformed(im.xf)))
}
_ => None,
})
.ok_or_else(|| bad("object.maskImage", "select an image"))?;
let (gid, pid) = s.edit("Mask", |d, sel| {
let mut clip = super::pathops::shape_node(d, outline, None);
super::object::as_clipping_path(&mut clip)?;
let pid = clip.id;
let (par, idx, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
let gid = d.alloc_id();
d.insert(par, idx + 1, Node::new(gid, NodeKind::Group { children: vec![], clip: true }))?;
d.insert(Some(gid), 0, clip)?;
d.move_node(id, Some(gid), usize::MAX)?;
sel.set([pid]);
Ok((gid, pid))
})?;
Ok(json!({ "id": gid.0, "path": pid.0 }))
}
// ---------- Trim marks ----------
impl Session {
+24
View File
@@ -234,6 +234,30 @@ fn crop_image_disabled_without_image() {
assert!(matches!(s.execute("object.cropImage", &json!({})), Err(EngineError::Disabled(..))));
}
#[test]
fn mask_image_clips_it_to_its_outline_and_selects_the_clipping_path() {
let mut s = session();
let a = rect(&mut s, 10.0, 20.0, 100.0, 50.0);
sel(&mut s, &[a]);
let img = id_of(&s.execute("object.rasterize", &json!({"ppi": 72})).unwrap());
s.execute("object.rotate", &json!({"angle": 30})).unwrap();
let ib = bounds(&s, img);
let r = s.execute("object.maskImage", &json!({})).unwrap();
let (g, p) = (id_of(&r), NodeId(r["path"].as_u64().unwrap()));
let group = node(&s, g);
assert!(matches!(group.kind, NodeKind::Group { clip: true, .. }));
assert_eq!(group.children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>(), [p, img]);
// The clipping path follows the rotated image's outline, so nothing is cut off yet.
assert!(matches!(node(&s, p).kind, NodeKind::Path { clipping: true, .. }));
let pb = bounds(&s, p);
assert!(close(pb.x0, ib.x0) && close(pb.y0, ib.y0) && close(pb.x1, ib.x1) && close(pb.y1, ib.y1), "{pb:?} {ib:?}");
assert_eq!(s.doc().unwrap().selection.objects, [p]);
s.execute("edit.undo", &json!({})).unwrap();
assert!(s.doc().unwrap().doc.node(g).is_none() && s.doc().unwrap().doc.node(img).is_some());
sel(&mut s, &[]);
assert!(matches!(s.execute("object.maskImage", &json!({})), Err(EngineError::Disabled(..))));
}
#[test]
fn trim_marks_around_selection() {
let mut s = session();
+18 -14
View File
@@ -135,6 +135,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
tl!("Embedded")
}
}
Some(n) if sel.len() == 1 && crate::panels::image_trace::is_trace(n) => tl!("Image Tracing"),
_ => tl!(crate::panels::appearance::object_label(app)),
};
ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text));
@@ -142,24 +143,27 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
crate::menus::invoke(app, "path.removeAnchors", json!({}));
}
ui.add_space(6.0);
crate::place::control_bar_details(app, ui);
// An image or an Image Trace object shows its own controls in place of Fill and Stroke.
let image = crate::place::control_bar_details(app, ui) || crate::panels::image_trace::control_bar(app, ui);
crate::toolbar::control_bar_options(app, ui);
crate::dialogs::envelope::control_bar(app, ui);
let shown_stroke = crate::panels::current_stroke(app);
let mixed = crate::panels::stroke_mixed(app, ui.ctx());
let weight = stroke_panel::shown_weight(app, shown_stroke.as_ref(), &mixed);
let opacity = if first.is_some() { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 };
crate::panels::paint_chip(app, ui, false, 22.0, true);
crate::panels::paint_chip(app, ui, true, 22.0, true);
// The link opens the Stroke panel as a popover under it; then the weight spinner
// (with presets) and the width profile.
stroke_panel::link(app, ui, tl!("Stroke:"));
stroke_panel::weight_field(app, ui, "cb-stroke", weight, 100.0);
if let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) {
app.run("stroke.set", json!({"profile": id})).ok();
if !image {
let shown_stroke = crate::panels::current_stroke(app);
let mixed = crate::panels::stroke_mixed(app, ui.ctx());
let weight = stroke_panel::shown_weight(app, shown_stroke.as_ref(), &mixed);
crate::panels::paint_chip(app, ui, false, 22.0, true);
crate::panels::paint_chip(app, ui, true, 22.0, true);
// The link opens the Stroke panel as a popover under it; then the weight spinner
// (with presets) and the width profile.
stroke_panel::link(app, ui, tl!("Stroke:"));
stroke_panel::weight_field(app, ui, "cb-stroke", weight, 100.0);
if let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) {
app.run("stroke.set", json!({"profile": id})).ok();
}
ui.add_space(4.0);
ui.separator();
}
ui.add_space(4.0);
ui.separator();
if ui.link(egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline()).clicked() {
app.ui.open_panel = Some("transparency".into());
}
+1
View File
@@ -132,6 +132,7 @@
Expand… Rozložit…
Expand Appearance Rozložit vzhled
Crop Image Oříznout obraz
Mask Maska
Rasterize… Rastrovat…
Create Gradient Mesh… Vytvořit přechodovou síť…
Create Object Mosaic… Vytvořit mozaiku objektů…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -511,6 +511,7 @@
Created: Creado:
Crop Recortar
Crop Image Recortar imagen
Mask Máscara
Crop Marks Marcas de corte
Crop To Recortar a
Crystallize Tool Herramienta Cristalizar
@@ -1143,6 +1144,8 @@
Image Map: Mapa de imagen:
Image Size Tamaño de imagen
Image Trace Calco de imagen
Image Tracing Objeto de calco de imagen
Tracing Presets Preajustes de calco
Image Trace Presets Preajustes de calco de imagen
Image: Imagen:
Images: Imágenes:
@@ -2669,6 +2672,7 @@
Total Width: Anchura total:
Touch Type Tool Herramienta Retocar texto
Trace Calcar
Trace the image with the Default preset Calcar la imagen con el preajuste Predeterminado
Tracking Espaciado
Trademark Symbol Símbolo de marca comercial
Transform Transformar
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -158,6 +158,7 @@
Expand… 分割・拡張…
Expand Appearance アピアランスを分割
Crop Image 画像の切り抜き
Mask マスク
Rasterize… ラスタライズ…
Create Gradient Mesh… グラデーションメッシュを作成…
Create Object Mosaic… モザイクオブジェクトを作成…
@@ -222,6 +223,8 @@
Merge 結合
Gap Options… 間隔オプション…
Image Trace 画像トレース
Image Tracing 画像トレースオブジェクト
Tracing Presets トレースプリセット
Make… 作成…
Make and Expand… 作成して拡張…
Text Wrap テキストの回り込み
@@ -2419,6 +2422,7 @@
Total Width: 合計幅:
Touch Type Tool 文字タッチツール
Trace トレース
Trace the image with the Default preset 「デフォルト」プリセットで画像をトレース
Tracking トラッキング
Transform Both 両方を変形
Transform Each 個別に変形
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -508,6 +508,7 @@
Created: Criado:
Crop Recortar
Crop Image Recortar Imagem
Mask Máscara
Crop Marks Marcas de Corte
Crop To Recortar Para
Crystallize Tool Ferramenta Cristalizar
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -509,6 +509,7 @@
Created: 创建时间:
Crop 裁剪
Crop Image 裁剪图像
Mask 蒙版
Crop Marks 裁切标记
Crop To 裁剪到
Crystallize Tool 晶格化工具
@@ -1129,6 +1130,8 @@
Image Map: 图像映射:
Image Size 图像大小
Image Trace 图像描摹
Image Tracing 图像描摹对象
Tracing Presets 描摹预设
Image Trace Presets 图像描摹预设
Image: 图像:
Images: 图像:
@@ -2605,6 +2608,7 @@
Total Width: 总宽度:
Touch Type Tool 修饰文字工具
Trace 描摹
Trace the image with the Default preset 使用“默认”预设描摹图像
Tracing 描摹
Tracking 字距
Trademark Symbol 商标符号
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -524,6 +524,7 @@
Created: 建立時間:
Crop 裁切
Crop Image 裁切影像
Mask 遮色片
Crop Marks 裁切標記
Crop To 裁切至
Crystallize Tool 結晶化工具
@@ -1158,6 +1159,8 @@
Image Map: 影像地圖:
Image Size 影像尺寸
Image Trace 影像描圖
Image Tracing 影像描圖物件
Tracing Presets 描圖預設集
Image Trace Presets 影像描圖預設集
Image: 影像:
Images: 影像:
@@ -2688,6 +2691,7 @@
Total Width: 總寬度:
Touch Type Tool 觸控文字工具
Trace 描圖
Trace the image with the Default preset 使用「預設」預設集描圖影像
Tracing 描圖中
Tracking 字距
Trademark Symbol 商標符號
Can't render this file because it contains an unexpected character in line 6 and column 47.
+75 -3
View File
@@ -4,6 +4,7 @@
use egui::Ui;
use serde_json::{Value, json};
use vectorcraft_doc::{Node, NodeKind};
use super::{first_selected, pstate, set_pstate};
use crate::VectorcraftApp;
@@ -30,12 +31,83 @@ fn presets(app: &mut VectorcraftApp) -> Vec<(String, Value)> {
.unwrap_or_default()
}
/// Whether `n` is an Image Trace object: a group so named whose first child is the traced image.
pub(crate) fn is_trace(n: &Node) -> bool {
matches!(n.kind, NodeKind::Group { clip: false, .. })
&& n.name.as_deref() == Some("Image Trace")
&& n.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, NodeKind::Image(_))))
}
/// What the selection is: an Image Trace object (with its stored settings), a plain image, or neither.
fn target(app: &VectorcraftApp) -> (bool, bool, Option<Value>) {
let Some(n) = first_selected(app) else { return (false, false, None) };
let trace = n.name.as_deref() == Some("Image Trace")
&& n.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, vectorcraft_doc::NodeKind::Image(_))));
(trace, matches!(n.kind, vectorcraft_doc::NodeKind::Image(_)), n.trace.map(|t| *t))
(is_trace(&n), matches!(n.kind, NodeKind::Image(_)), n.trace.map(|t| *t))
}
/// The preset of the one selected Image Trace object ("Custom" once its settings were changed).
pub(crate) fn selected_preset(app: &VectorcraftApp) -> Option<String> {
let st = app.session.active()?;
let [id] = st.selection.objects[..] else { return None };
let n = st.doc.node(id).filter(|n| is_trace(n))?;
Some(n.trace.as_ref().and_then(|t| t["preset"].as_str()).unwrap_or("Custom").to_string())
}
/// Trace the selected image, or trace the selected Image Trace object again, with built-in `preset`.
fn trace_with(app: &mut VectorcraftApp, preset: &str) {
// A failure is reported in the status bar.
let _ = app.run("imageTrace.make", json!({ "preset": preset }));
}
/// The built-in presets to choose from (`current` highlighted) → the one chosen. Listed only
/// while the menu is open.
fn preset_list(app: &mut VectorcraftApp, ui: &mut Ui, current: &str) -> Option<String> {
let mut chosen = None;
for (name, _) in presets(app) {
if ui.add(egui::Button::selectable(name == current, tl!(&name))).clicked() {
chosen = Some(name);
}
}
chosen
}
/// Width of the Control bar's Image Trace button.
pub(crate) const TRACE_BUTTON_W: f32 = 104.0;
/// The Image Trace button for a selected image (Control bar, Properties): a click traces it with
/// the Default preset, its arrow lists the presets to trace it with.
pub(crate) fn trace_button(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) {
let (main, arrow) = widgets::split_button(ui, tl!("Image Trace"), width);
if main.on_hover_text(tl!("Trace the image with the Default preset")).clicked() {
trace_with(app, "Default");
}
let arrow = arrow.on_hover_text(tl!("Tracing Presets"));
if let Some(p) = egui::Popup::menu(&arrow).show(|ui| preset_list(app, ui, "")).and_then(|r| r.inner) {
trace_with(app, &p);
}
}
/// The selected Image Trace object's preset (Control bar, Properties): choosing another traces it
/// again with that preset.
pub(crate) fn preset_dropdown(app: &mut VectorcraftApp, ui: &mut Ui, current: &str, width: f32) {
if let Some(p) = widgets::combo(ui, "trace-preset", tl!(current), width, false, |ui| preset_list(app, ui, current)) {
trace_with(app, &p);
}
}
/// The Control bar for one selected Image Trace object: its preset, the Image Trace panel and
/// Expand. Whether it is one.
pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) -> bool {
let Some(preset) = selected_preset(app) else { return false };
widgets::dim_label(ui, tl!("Preset:"));
preset_dropdown(app, ui, &preset, 150.0);
if widgets::icon_button(ui, "image", tl!("Image Trace"), false, 24.0).clicked() {
app.ui.open_panel = Some("imageTrace".into());
}
if widgets::flat_button(ui, tl!("Expand"), 64.0).clicked() {
app.run("imageTrace.expand", json!({})).ok();
}
ui.separator();
true
}
fn trace(app: &mut VectorcraftApp, st: &mut TraceUi) {
+31 -18
View File
@@ -21,6 +21,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let label = match (&first, n_sel) {
(None, _) => tl!("Document").to_string(),
(_, n) if n > 1 => crate::i18n::tn(n as u64, "{n} Object", "{n} Objects"),
(Some(n), _) if crate::panels::image_trace::is_trace(n) => tl!("Image Tracing").to_string(),
(Some(n), _) => tl!(n.kind_label()).to_string(),
};
ui.label(egui::RichText::new(label).size(11.5).color(t.text_dim));
@@ -31,10 +32,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
transform_section(app, ui);
divider(ui);
if n_sel == 1 && matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Image(_))) {
let is_image = n_sel == 1 && matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Image(_)));
if is_image {
image_section(app, ui);
divider(ui);
}
if let Some(preset) = crate::panels::image_trace::selected_preset(app) {
trace_section(app, ui, &preset);
divider(ui);
}
if matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Text(_))) {
type_sections(app, ui);
divider(ui);
@@ -102,6 +108,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
if anchor_mode && widgets::flat_button(ui, "Remove Anchor Points", ui.available_width()).clicked() {
crate::menus::invoke(app, "path.removeAnchors", json!({}));
}
if is_image {
crate::panels::image_trace::trace_button(app, ui, ui.available_width());
actions.push((tl!("Crop Image"), "object.cropImage"));
actions.push((tl!("Mask"), "object.maskImage"));
}
actions.push((tl!("Offset Path"), "object.path.offsetPath"));
actions.push((tl!("Simplify"), "object.path.simplify"));
actions.push((tl!("Arrange: Bring to Front"), "object.arrange.bringToFront"));
@@ -131,23 +142,7 @@ fn image_section(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.add_space(4.0);
let w = (ui.available_width() - 6.0) / 2.0;
let links = crate::panels::links::ID;
ui.horizontal(|ui| {
if linked {
if widgets::flat_button(ui, tl!("Embed"), w).clicked() {
app.run("links.embed", json!({ "ids": [id] })).ok();
}
if widgets::flat_button(ui, tl!("Edit Original"), w).clicked() {
crate::menus::invoke(app, "links.editOriginal", json!({ "id": id }));
}
} else {
if widgets::flat_button(ui, tl!("Unembed…"), w).clicked() {
crate::panels::links::unembed(app, id, &name);
}
if widgets::flat_button(ui, tl!("Image Trace"), w).clicked() {
app.ui.open_panel = Some("imageTrace".into());
}
}
});
ui.horizontal(|ui| crate::place::link_buttons(app, ui, id, linked, &name, Some(w)));
ui.horizontal(|ui| {
if app.services.pick_open.is_some() && widgets::flat_button(ui, tl!("Relink…"), w).clicked() {
crate::panels::links::relink(app, vec![id]);
@@ -158,6 +153,24 @@ fn image_section(app: &mut VectorcraftApp, ui: &mut Ui) {
});
}
/// The one selected Image Trace object: its preset (choosing another traces it again with that
/// one), Expand and Release.
fn trace_section(app: &mut VectorcraftApp, ui: &mut Ui, preset: &str) {
section_header(ui, tl!("Image Trace"));
ui.horizontal(|ui| {
dim_label(ui, tl!("Preset:"));
crate::panels::image_trace::preset_dropdown(app, ui, preset, ui.available_width());
});
let w = (ui.available_width() - 6.0) / 2.0;
ui.horizontal(|ui| {
for (label, id) in [(tl!("Expand"), "imageTrace.expand"), (tl!("Release"), "imageTrace.release")] {
if widgets::flat_button(ui, label, w).clicked() {
app.run(id, json!({})).ok();
}
}
});
}
/// Whether the selection uses more than one colour (`recolor.colors`), which offers the Recolor
/// quick action; cached per document revision.
fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
+33 -6
View File
@@ -10,7 +10,7 @@ use vectorcraft_engine::cmd::fileio;
use vectorcraft_geom::Point;
use crate::theme::{self, Tokens};
use crate::{VectorcraftApp, io};
use crate::{VectorcraftApp, io, widgets};
/// The loaded place cursor's thumbnail size (px on the longer side).
const THUMB: u32 = 64;
@@ -386,14 +386,41 @@ pub fn image_summary(i: &Value) -> (String, String) {
(name, format!("{} PPI: {}", i["colorMode"].as_str().unwrap_or("RGB"), fmt_ppi(ppi)))
}
/// The Control bar's details for the one selected image: Linked File or Embedded, its file name,
/// colour mode and effective resolution.
pub fn control_bar_details(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
let Some(i) = selected_image_info(app) else { return };
/// The Control bar for the one selected image: Linked File or Embedded, its file name, colour mode
/// and effective resolution, Embed or Unembed, Edit Original (a linked file), Image Trace, Mask and
/// Crop Image. Whether one image is selected.
pub fn control_bar_details(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
let Some(i) = selected_image_info(app) else { return false };
let (name, details) = image_summary(i);
let (Some(id), linked) = (i["id"].as_u64(), i["linked"] == true) else { return false };
let t = Tokens::get(ui.ctx());
ui.label(egui::RichText::new(name).size(12.0).color(t.text_strong));
ui.label(egui::RichText::new(&name).size(12.0).color(t.text_strong));
ui.separator();
ui.label(egui::RichText::new(details).size(12.0).color(t.text_dim));
ui.separator();
link_buttons(app, ui, id, linked, &name, None);
crate::panels::image_trace::trace_button(app, ui, crate::panels::image_trace::TRACE_BUTTON_W);
for (label, cmd, w) in [(tl!("Mask"), "object.maskImage", 52.0), (tl!("Crop Image"), "object.cropImage", 84.0)] {
if widgets::flat_button(ui, label, w).clicked() {
crate::menus::invoke(app, cmd, json!({}));
}
}
ui.separator();
true
}
/// Embed (and Edit Original) for linked image `id`, Unembed… for an embedded one named `name`
/// (Control bar, Properties). The buttons are `width` wide, or fit their labels.
pub fn link_buttons(app: &mut VectorcraftApp, ui: &mut egui::Ui, id: u64, linked: bool, name: &str, width: Option<f32>) {
let w = |fit: f32| width.unwrap_or(fit);
if linked {
if widgets::flat_button(ui, tl!("Embed"), w(60.0)).clicked() {
app.run("links.embed", json!({ "ids": [id] })).ok();
}
if widgets::flat_button(ui, tl!("Edit Original"), w(92.0)).clicked() {
crate::menus::invoke(app, "links.editOriginal", json!({ "id": id }));
}
} else if widgets::flat_button(ui, tl!("Unembed…"), w(80.0)).clicked() {
crate::panels::links::unembed(app, id, name);
}
}
+63
View File
@@ -245,3 +245,66 @@ fn the_control_bar_shows_the_image_file_link_colour_mode_and_ppi() {
let text = crate::tests_labels::painted_text(&mut app, crate::chrome::control_bar);
assert!(text.contains("Embedded"), "{text}");
}
/// Click the Control bar's `label`.
fn click_label(app: &mut VectorcraftApp, ctx: &egui::Context, label: &str) {
use crate::tests_removeanchors::{at, click_control, control_frame};
let p = at(&control_frame(app, ctx, vec![]), label);
click_control(app, ctx, p);
}
#[test]
fn the_control_bar_and_properties_trace_a_selected_image() {
use crate::panels::image_trace::{TRACE_BUTTON_W, selected_preset};
use crate::tests_removeanchors::{at, click_control, control_frame, has, properties_frame};
let mut app = app();
let data = vectorcraft_format::base64_encode(&png(30, 30, 72.0));
app.run("file.place", json!({"name": "red.png", "dataBase64": data, "link": false})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let bar = control_frame(&mut app, &ctx, vec![]);
for s in ["Embedded", "Unembed…", "Image Trace", "Mask", "Crop Image", "Opacity:"] {
assert!(has(&bar, s), "{s}");
}
assert!(!has(&bar, "Stroke:"), "an image has no Fill and Stroke in the Control bar");
let props = properties_frame(&mut app, 260.0);
for s in ["Unembed…", "Image Trace", "Mask", "Crop Image"] {
assert!(has(&props, s), "Properties: {s}");
}
// The button's arrow lists the presets; choosing one traces the image with it.
click_control(&mut app, &ctx, at(&bar, "Image Trace") + vec2(TRACE_BUTTON_W / 2.0, 0.0));
click_label(&mut app, &ctx, "6 Colors");
assert_eq!(selected_preset(&app).as_deref(), Some("6 Colors"));
// The button itself traces with the Default preset.
app.run("edit.undo", json!({})).unwrap();
click_label(&mut app, &ctx, "Image Trace");
assert_eq!(selected_preset(&app).as_deref(), Some("Default"));
// An Image Trace object: its preset (another traces it again), the panel and Expand.
let bar = control_frame(&mut app, &ctx, vec![]);
for s in ["Image Tracing", "Preset:", "Default", "Expand"] {
assert!(has(&bar, s), "{s}");
}
assert!(has(&properties_frame(&mut app, 260.0), "Release"));
click_control(&mut app, &ctx, at(&bar, "Default"));
click_label(&mut app, &ctx, "3 Colors");
assert_eq!(selected_preset(&app).as_deref(), Some("3 Colors"));
click_label(&mut app, &ctx, "Expand");
let st = app.session.active().unwrap();
let g = st.doc.node(st.selection.objects[0]).unwrap();
assert!(selected_preset(&app).is_none() && g.children().unwrap().iter().all(|c| !matches!(c.kind, NodeKind::Image(_))), "expanded");
}
#[test]
fn the_control_bars_mask_clips_the_image_and_selects_the_clipping_path() {
let mut app = app();
let data = vectorcraft_format::base64_encode(&png(30, 30, 72.0));
let img = app.run("file.place", json!({"name": "red.png", "dataBase64": data, "link": false})).unwrap()["ids"][0].as_u64().unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
click_label(&mut app, &ctx, "Mask");
let st = app.session.active().unwrap();
let group = &st.doc.layers[0].children().unwrap()[0];
assert!(matches!(group.kind, NodeKind::Group { clip: true, .. }));
let ids: Vec<u64> = group.children().unwrap().iter().map(|c| c.id.0).collect();
assert_eq!((ids[1], st.selection.objects.iter().map(|i| i.0).collect::<Vec<_>>()), (img, vec![ids[0]]));
}
+5 -5
View File
@@ -49,14 +49,14 @@ fn shapes_text(shapes: &[Shape]) -> Vec<(String, Rect)> {
v
}
fn control_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>) -> Vec<(String, Rect)> {
pub(crate) fn control_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>) -> Vec<(String, Rect)> {
let screen = Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0));
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| chrome::control_bar(app, ui));
out.textures_delta.clear();
shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>())
}
fn click_control(app: &mut VectorcraftApp, ctx: &egui::Context, at: Pos2) {
pub(crate) fn click_control(app: &mut VectorcraftApp, ctx: &egui::Context, at: Pos2) {
let press = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
control_frame(app, ctx, vec![Event::PointerMoved(at), press(true)]);
control_frame(app, ctx, vec![press(false)]);
@@ -78,7 +78,7 @@ fn click_canvas(app: &mut VectorcraftApp, ctx: &egui::Context, at: Pos2, button:
canvas_frame(app, ctx, vec![])
}
fn properties_frame(app: &mut VectorcraftApp, width: f32) -> Vec<(String, Rect)> {
pub(crate) fn properties_frame(app: &mut VectorcraftApp, width: f32) -> Vec<(String, Rect)> {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let screen = Rect::from_min_size(Pos2::ZERO, vec2(width, 900.0));
@@ -89,11 +89,11 @@ fn properties_frame(app: &mut VectorcraftApp, width: f32) -> Vec<(String, Rect)>
shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>())
}
fn has(texts: &[(String, Rect)], label: &str) -> bool {
pub(crate) fn has(texts: &[(String, Rect)], label: &str) -> bool {
texts.iter().any(|(t, _)| t == label)
}
fn at(texts: &[(String, Rect)], label: &str) -> Pos2 {
pub(crate) fn at(texts: &[(String, Rect)], label: &str) -> Pos2 {
texts.iter().find(|(t, _)| t == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no `{label}`"))
}
+41 -5
View File
@@ -29,11 +29,7 @@ pub fn flat_button(ui: &mut Ui, text: &str, width: f32) -> Response {
let t = Tokens::get(ui.ctx());
let (rect, resp) = ui.allocate_exact_size(vec2(width, 24.0), Sense::click());
let rect = rect.shrink2(vec2(0.0, 0.5));
if resp.is_pointer_button_down_on() {
ui.painter().rect_filled(rect, CornerRadius::same(2), t.tool_active);
} else if resp.hovered() {
ui.painter().rect_filled(rect, CornerRadius::same(2), t.hover);
}
flat_fill(ui, rect, &resp);
ui.painter().rect_stroke(
rect,
CornerRadius::same(2),
@@ -44,6 +40,46 @@ pub fn flat_button(ui: &mut Ui, text: &str, width: f32) -> Response {
resp
}
/// Width of a [`split_button`]'s arrow.
pub const SPLIT_ARROW: f32 = 18.0;
/// A [`flat_button`] `width` wide whose right end is a menu arrow (Image Trace ▾) → the
/// responses of the button and of its arrow.
pub fn split_button(ui: &mut Ui, text: &str, width: f32) -> (Response, Response) {
let t = Tokens::get(ui.ctx());
let (rect, _) = ui.allocate_exact_size(vec2(width, 24.0), Sense::hover());
let rect = rect.shrink2(vec2(0.0, 0.5));
let split = rect.right() - SPLIT_ARROW;
let main_rect = Rect::from_min_max(rect.min, pos2(split, rect.bottom()));
let arrow_rect = Rect::from_min_max(pos2(split, rect.top()), rect.max);
let main = ui.interact(main_rect, ui.id().with((text, "split")), Sense::click());
let arrow = ui.interact(arrow_rect, ui.id().with((text, "split-arrow")), Sense::click());
flat_fill(ui, main_rect, &main);
flat_fill(ui, arrow_rect, &arrow);
let border = if main.hovered() || arrow.hovered() { t.text } else { t.button_border };
ui.painter().rect_stroke(rect, CornerRadius::same(2), Stroke::new(1.0, border), StrokeKind::Inside);
ui.painter().vline(split, rect.y_range().shrink(4.0), Stroke::new(1.0, t.button_border));
ui.painter().with_clip_rect(main_rect).text(
main_rect.center(),
egui::Align2::CENTER_CENTER,
tl!(text),
egui::FontId::proportional(12.5),
t.text_strong,
);
icons::paint(ui, "chevron-down", Rect::from_center_size(arrow_rect.center(), vec2(12.0, 12.0)), t.text_dim);
(main, arrow)
}
/// The pressed or hovered fill of a flat button part.
fn flat_fill(ui: &Ui, rect: Rect, resp: &Response) {
let t = Tokens::get(ui.ctx());
if resp.is_pointer_button_down_on() {
ui.painter().rect_filled(rect, CornerRadius::same(2), t.tool_active);
} else if resp.hovered() {
ui.painter().rect_filled(rect, CornerRadius::same(2), t.hover);
}
}
/// Blue call-to-action pill (dialog OK/Create).
pub fn primary_button(ui: &mut Ui, text: &str) -> Response {
let t = Tokens::get(ui.ctx());