Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d013959cb | ||
|
|
857273001b | ||
|
|
48f07958a9 | ||
|
|
c6bea61ffc | ||
|
|
c44572d272 | ||
|
|
1b43d8b8f9 |
No files matched your search
+1
-1
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-09 (after 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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1492,6 +1492,8 @@
|
||||
New Art Brush from Selection Nuevo pincel de arte a partir de la selección
|
||||
New Art Has Basic Appearance La nueva ilustración tiene apariencia básica
|
||||
New Artboard Nueva mesa de trabajo
|
||||
Artboard Mesa de trabajo
|
||||
Exit Salir
|
||||
New Bristle Brush Nuevo pincel de cerdas
|
||||
New Brush… Nuevo pincel…
|
||||
New Calligraphic Brush Nuevo pincel caligráfico
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1492,6 +1492,8 @@
|
||||
New Art Brush from Selection Nuovo pennello artistico dalla selezione
|
||||
New Art Has Basic Appearance La nuova grafica ha aspetto di base
|
||||
New Artboard Nuova tavola da disegno
|
||||
Artboard Tavola da disegno
|
||||
Exit Esci
|
||||
New Bristle Brush Nuovo pennello a setole
|
||||
New Brush… Nuovo pennello…
|
||||
New Calligraphic Brush Nuovo pennello calligrafico
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1635,6 +1635,8 @@
|
||||
New Art Brush from Selection 選択範囲から新規アートブラシ
|
||||
New Art Has Basic Appearance 新規アートに基本アピアランスを使用
|
||||
New Artboard 新規アートボード
|
||||
Artboard アートボード
|
||||
Exit 終了
|
||||
New Bristle Brush 新規剛毛ブラシ
|
||||
New Brush… 新規ブラシ…
|
||||
New Calligraphic Brush 新規絵筆ブラシ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1469,6 +1469,7 @@
|
||||
New Art Brush from Selection 从选区新建艺术画笔
|
||||
New Art Has Basic Appearance 新建图稿使用基本外观
|
||||
New Artboard 新建画板
|
||||
Exit 退出
|
||||
New Bristle Brush 新建鬃毛画笔
|
||||
New Brush… 新建画笔…
|
||||
New Calligraphic Brush 新建书法画笔
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1509,6 +1509,8 @@
|
||||
New Art Brush from Selection 從選取範圍新增藝術筆刷
|
||||
New Art Has Basic Appearance 新圖稿具有基本外觀
|
||||
New Artboard 新增工作區域
|
||||
Artboard 工作區域
|
||||
Exit 結束
|
||||
New Bristle Brush 新增鬃毛筆刷
|
||||
New Brush… 新增筆刷…
|
||||
New Calligraphic Brush 新增書法筆刷
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -13,13 +13,13 @@ use crate::widgets::{self, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
/// The active artboard (of `n`).
|
||||
fn selected(app: &VectorcraftApp, n: usize) -> usize {
|
||||
pub(crate) fn selected(app: &VectorcraftApp, n: usize) -> usize {
|
||||
app.view().map_or(0, |v| v.artboard).min(n.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// Make artboard `i` the active one, leaving the view where it is: the navigator's and, while it
|
||||
/// is the tool, the Artboard tool's.
|
||||
fn select(app: &mut VectorcraftApp, i: usize) {
|
||||
pub(crate) fn select(app: &mut VectorcraftApp, i: usize) {
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = i;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let n_sel = st.selection.len();
|
||||
let anchors = crate::chrome::anchor_controls(app);
|
||||
let first = first_selected(app);
|
||||
// Edit Artboards (the Artboard tool): the active artboard, whatever is selected.
|
||||
let editing_artboards = app.session.tool_id() == "artboard";
|
||||
let label = match (&first, n_sel) {
|
||||
_ if editing_artboards => tl!("Artboard").to_string(),
|
||||
(None, _) => tl!("Document").to_string(),
|
||||
(_, n) if n > 1 => crate::i18n::tn(n as u64, "{n} Object", "{n} Objects"),
|
||||
(Some(n), _) if crate::panels::image_trace::is_trace(n) => tl!("Image Tracing").to_string(),
|
||||
@@ -26,6 +29,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
};
|
||||
ui.label(egui::RichText::new(label).size(11.5).color(t.text_dim));
|
||||
ui.add_space(4.0);
|
||||
if editing_artboards {
|
||||
artboard_sections(app, ui);
|
||||
return;
|
||||
}
|
||||
if first.is_none() {
|
||||
document_sections(app, ui);
|
||||
return;
|
||||
@@ -188,6 +195,48 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit Artboards (the Artboard tool): the active artboard's name, position and size, New Artboard
|
||||
/// and Delete Artboard, and Exit back to the Selection tool (#530).
|
||||
fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let units = app.session.general_unit();
|
||||
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
|
||||
let i = super::artboards::selected(app, n);
|
||||
if let Some(ab) = app.session.active().and_then(|d| d.doc.artboards.get(i)).cloned() {
|
||||
// Its name as it is (names are never translated).
|
||||
ui.label(egui::RichText::new(&ab.name).size(13.0).color(Tokens::get(ui.ctx()).text));
|
||||
let fw = ((ui.available_width() - 50.0) / 2.0).clamp(60.0, 110.0);
|
||||
egui::Grid::new("ab-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
let r = ab.rect;
|
||||
for (row, [(l1, k1, v1), (l2, k2, v2)]) in
|
||||
[[("X:", "x", r.x0), ("W:", "width", r.width())], [("Y:", "y", r.y0), ("H:", "height", r.height())]].into_iter().enumerate()
|
||||
{
|
||||
for (label, key, v) in [(l1, k1, v1), (l2, k2, v2)] {
|
||||
dim_label(ui, label);
|
||||
if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v), units, fw) {
|
||||
app.run("artboard.setProps", json!({"index": i, key: v})).ok();
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
let w = (ui.available_width() - 6.0) / 2.0;
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::flat_button(ui, tl!("New Artboard"), w).clicked() && app.run("artboard.new", json!({})).is_ok() {
|
||||
super::artboards::select(app, n);
|
||||
}
|
||||
if ui.add_enabled_ui(n > 1, |ui| widgets::flat_button(ui, tl!("Delete Artboard"), w)).inner.clicked() {
|
||||
app.run("artboard.delete", json!({"index": i})).ok();
|
||||
}
|
||||
});
|
||||
divider(ui);
|
||||
section_header(ui, tl!("Quick Actions"));
|
||||
if widgets::flat_button(ui, tl!("Exit"), w).clicked() {
|
||||
app.select_tool("selection");
|
||||
}
|
||||
}
|
||||
|
||||
fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let units = app.session.general_unit();
|
||||
section_header(ui, tl!("Document"));
|
||||
@@ -416,3 +465,58 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{Event, PointerButton, Pos2, Rect, vec2};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// One frame of the panel with `events` → the texts painted, with their rects.
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>) -> Vec<(String, Rect)> {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if let egui::Shape::Text(t) = &s.shape {
|
||||
Some((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, texts: &[(String, Rect)], label: &str) {
|
||||
let at = texts.iter().find(|(t, _)| t == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"));
|
||||
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(app, ctx, vec![Event::PointerMoved(at), b(true)]);
|
||||
frame(app, ctx, vec![b(false)]);
|
||||
}
|
||||
|
||||
/// #530: Edit Artboards shows the active artboard, with New Artboard, Delete Artboard and Exit.
|
||||
#[test]
|
||||
fn edit_artboards_adds_artboards_and_exits() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
click(&mut app, &ctx, &texts, "Edit Artboards");
|
||||
assert_eq!(app.session.tool_id(), "artboard");
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Artboard 1") && texts.iter().any(|(t, _)| t == "200 pt"), "{texts:?}");
|
||||
click(&mut app, &ctx, &texts, "New Artboard");
|
||||
let boards = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.len();
|
||||
assert_eq!(boards(&app), 2);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Artboard 2"), "the new artboard is the active one: {texts:?}");
|
||||
click(&mut app, &ctx, &texts, "Delete Artboard");
|
||||
assert_eq!(boards(&app), 1);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
click(&mut app, &ctx, &texts, "Exit");
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user