mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 08:20:13 +00:00
Panels: long swatch, preset and Add Effect popups scroll inside a short window
The gradient swatches menu, the Image Trace button's list of presets, the Appearance panel's swatch picker and the top level of the Add New Effect popup (Appearance panel, Properties) had no length limit, so rows past the bottom of a short window could not be reached. Each now uses menu_scroll. Items, order and commands do not change, and a list that fits does not scroll.
This commit is contained in:
@@ -661,6 +661,8 @@ mod tests_freeform;
|
||||
#[cfg(test)]
|
||||
mod tests_links;
|
||||
#[cfg(test)]
|
||||
mod tests_long_popups;
|
||||
#[cfg(test)]
|
||||
mod tests_maskview;
|
||||
#[cfg(test)]
|
||||
mod tests_stroke;
|
||||
|
||||
@@ -1332,7 +1332,7 @@ fn swatch_button(app: &mut VectorcraftApp, ui: &mut Ui, r: Rect, paint: &Paint,
|
||||
open_popup(app, ui.ctx(), "color");
|
||||
}
|
||||
egui::Popup::menu(&resp).open_memory((resp.clicked() && !mixer).then_some(egui::SetOpenCommand::Toggle)).show(|ui| {
|
||||
swatch_picker(app, ui, target);
|
||||
widgets::menu_scroll(ui, |ui| swatch_picker(app, ui, target));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1608,18 +1608,20 @@ pub(crate) fn fx_menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
None => groups.push((g, vec![(id.clone(), label.to_string())])),
|
||||
}
|
||||
}
|
||||
for (g, items) in groups {
|
||||
ui.menu_button(tl!(&g), |ui| {
|
||||
crate::widgets::menu_scroll(ui, |ui| {
|
||||
for (id, label) in items {
|
||||
if ui.button(format!("{label}…")).clicked() {
|
||||
app.run("effect.dialog", json!({"effect": id})).ok();
|
||||
ui.close();
|
||||
crate::widgets::menu_scroll(ui, |ui| {
|
||||
for (g, items) in groups {
|
||||
ui.menu_button(tl!(&g), |ui| {
|
||||
crate::widgets::menu_scroll(ui, |ui| {
|
||||
for (id, label) in items {
|
||||
if ui.button(format!("{label}…")).clicked() {
|
||||
app.run("effect.dialog", json!({"effect": id})).ok();
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
@@ -1,5 +1,86 @@
|
||||
use super::super::tests_long_popups::{PopupWindow, assert_last_row_scrolls_into_view, assert_short_list_does_not_scroll, fx_menu_ends};
|
||||
use super::*;
|
||||
|
||||
/// A selected rectangle, for the fill row's swatch picker and the Add Effect button.
|
||||
fn app_with_a_selected_rectangle() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({ "ids": [id] })).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
/// The swatch of a fill row at the top of the window, with the size of its click target.
|
||||
fn swatch_button_at_the_top() -> (impl FnMut(&mut VectorcraftApp, &mut Ui), egui::Vec2) {
|
||||
let row = Rect::from_min_size(pos2(0.0, 8.0), vec2(400.0, 22.0));
|
||||
(move |app: &mut VectorcraftApp, ui: &mut Ui| swatch_button(app, ui, row, &Paint::None, Painted::Item(0)), vec2(22.0, 22.0))
|
||||
}
|
||||
|
||||
/// One tile of the swatch picker.
|
||||
const TILE: egui::Vec2 = vec2(15.5, 15.5);
|
||||
|
||||
/// The picker has one tile per swatch: with enough of them it stops inside a short window and
|
||||
/// scrolls, its rows stay 12 tiles wide, and a click on the last tile paints with the last swatch.
|
||||
#[test]
|
||||
fn a_long_swatch_picker_scrolls_inside_a_short_window() {
|
||||
const HEIGHT: f32 = 200.0;
|
||||
let mut app = app_with_a_selected_rectangle();
|
||||
for n in 0..150 {
|
||||
app.run("swatch.new", json!({"name": format!("Long {n}"), "color": format!("#{:06x}", n + 1)})).unwrap();
|
||||
}
|
||||
let doc = &app.session.active().unwrap().doc;
|
||||
let last_swatch = doc.swatches.iter().chain(doc.swatch_groups.iter().flat_map(|group| &group.swatches)).last().unwrap().paint.clone();
|
||||
let (draw, chip) = swatch_button_at_the_top();
|
||||
let mut window = PopupWindow::new(&mut app, HEIGHT, draw);
|
||||
window.click(&mut app, window.clickable(chip)[0].center());
|
||||
let tiles = window.clickable(TILE);
|
||||
assert!(tiles.len() > 150, "one tile per swatch: {}", tiles.len());
|
||||
assert_eq!(tiles[11].top(), tiles[0].top(), "12 tiles in a row");
|
||||
assert!(tiles[12].top() > tiles[0].top(), "the 13th tile starts the next row");
|
||||
assert!(tiles[tiles.len() - 1].bottom() > HEIGHT, "the picker opens at the top of its list");
|
||||
window.wheel_down(&mut app, tiles[0].center());
|
||||
let tiles = window.clickable(TILE);
|
||||
let last = tiles[tiles.len() - 1];
|
||||
assert!(last.bottom() <= HEIGHT, "the last tile at {last:?} is inside the window");
|
||||
window.click(&mut app, last.center());
|
||||
let node = first_subject(&app).expect("the rectangle");
|
||||
let (Paint::Solid { color, .. }, Paint::Solid { color: last_color, .. }) = (node.appearance.items[0].paint(), &last_swatch) else {
|
||||
panic!("solid paints")
|
||||
};
|
||||
assert_eq!(color, last_color);
|
||||
}
|
||||
|
||||
/// A picker with few swatches fits its tiles and the wheel does not move them.
|
||||
#[test]
|
||||
fn a_short_swatch_picker_does_not_scroll() {
|
||||
let mut app = app_with_a_selected_rectangle();
|
||||
let (draw, chip) = swatch_button_at_the_top();
|
||||
let mut window = PopupWindow::new(&mut app, 900.0, draw);
|
||||
window.click(&mut app, window.clickable(chip)[0].center());
|
||||
let tiles = window.clickable(TILE);
|
||||
assert!(tiles[tiles.len() - 1].bottom() <= 900.0, "every tile is inside the window");
|
||||
window.wheel_down(&mut app, tiles[0].center());
|
||||
assert_eq!(window.clickable(TILE), tiles, "a list that fits does not scroll");
|
||||
}
|
||||
|
||||
/// The Add New Effect button: the third of the bottom bar's 24 pt buttons.
|
||||
fn fx_button(window: &PopupWindow) -> egui::Pos2 {
|
||||
window.clickable(vec2(24.0, 24.0))[2].center()
|
||||
}
|
||||
|
||||
fn draw_bottom_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let node = first_subject(app);
|
||||
bottom(app, ui, node.as_ref(), Sel::None);
|
||||
}
|
||||
|
||||
/// The Add New Effect popup has one row per effect group, plug-in groups included.
|
||||
#[test]
|
||||
fn the_add_effect_popup_scrolls_inside_a_short_window() {
|
||||
let (first, last) = fx_menu_ends();
|
||||
assert_last_row_scrolls_into_view(&mut app_with_a_selected_rectangle(), 150.0, draw_bottom_bar, fx_button, &first, &last);
|
||||
assert_short_list_does_not_scroll(&mut app_with_a_selected_rectangle(), draw_bottom_bar, fx_button, &first, &last);
|
||||
}
|
||||
|
||||
/// Duplicating rows highlights the copies, each right above its original, as the Layers panel
|
||||
/// does: the active row's copy becomes active, the anchor's copy the anchor.
|
||||
#[test]
|
||||
|
||||
@@ -547,26 +547,28 @@ fn thumbnail(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad:
|
||||
icons::paint(ui, "chevron-down", Rect::from_center_size(dr.center(), vec2(12.0, 12.0)), if dresp.hovered() { t.text_strong } else { t.icon });
|
||||
let dresp = dresp.on_hover_text(tl!("Gradient swatches"));
|
||||
egui::Popup::menu(&dresp).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
let mut chosen = None;
|
||||
let swatches = gradient_swatches(app);
|
||||
if swatches.is_empty() {
|
||||
widgets::dim_label(ui, tl!("No gradient swatches"));
|
||||
}
|
||||
for (name, grad) in &swatches {
|
||||
// The gradient swatch the gradient was applied from is lit.
|
||||
if gradient_row(ui, &t, grad, name, g.swatch.as_ref() == Some(name)).clicked() {
|
||||
chosen = Some(name.clone());
|
||||
widgets::menu_scroll(ui, |ui| {
|
||||
ui.set_min_width(200.0);
|
||||
let mut chosen = None;
|
||||
let swatches = gradient_swatches(app);
|
||||
if swatches.is_empty() {
|
||||
widgets::dim_label(ui, tl!("No gradient swatches"));
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if menu_item(ui, tl!("Save to Swatches"), is_grad, false) {
|
||||
save_to_swatches(app, g);
|
||||
}
|
||||
if let Some(name) = chosen {
|
||||
super::apply_click(app, ui, json!({ "swatch": name }));
|
||||
ui.close();
|
||||
}
|
||||
for (name, grad) in &swatches {
|
||||
// The gradient swatch the gradient was applied from is lit.
|
||||
if gradient_row(ui, &t, grad, name, g.swatch.as_ref() == Some(name)).clicked() {
|
||||
chosen = Some(name.clone());
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if menu_item(ui, tl!("Save to Swatches"), is_grad, false) {
|
||||
save_to_swatches(app, g);
|
||||
}
|
||||
if let Some(name) = chosen {
|
||||
super::apply_click(app, ui, json!({ "swatch": name }));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::super::tests_long_popups::{PopupWindow, assert_last_row_scrolls_into_view, assert_short_list_does_not_scroll};
|
||||
use super::*;
|
||||
|
||||
/// The gradient swatches menu has one row per gradient swatch: with enough of them it stops inside a
|
||||
/// short window and the wheel reaches Save to Swatches. With few of them it does not scroll.
|
||||
#[test]
|
||||
fn a_long_gradient_swatch_menu_scrolls_inside_a_short_window() {
|
||||
let stops = json!([{"offset": 0, "color": "#000000"}, {"offset": 1, "color": "#ffffff"}]);
|
||||
let mut app = app(json!({"gradient": {"stops": stops}}));
|
||||
app.run("swatch.new", json!({"name": "Dusk", "gradient": {"stops": stops}})).unwrap();
|
||||
let paint = current(&app).expect("a gradient fill");
|
||||
let short = paint.clone();
|
||||
let arrow = |window: &PopupWindow| window.clickable(vec2(14.0, 40.0))[0].center();
|
||||
let first = gradient_swatches(&app)[0].0.clone();
|
||||
assert_short_list_does_not_scroll(&mut app, move |app, ui| thumbnail(app, ui, &short, true), arrow, &first, "Save to Swatches");
|
||||
for n in 0..30 {
|
||||
app.run("swatch.new", json!({"name": format!("Long {n}"), "gradient": {"stops": stops}})).unwrap();
|
||||
}
|
||||
assert_last_row_scrolls_into_view(&mut app, 200.0, move |app, ui| thumbnail(app, ui, &paint, true), arrow, &first, "Save to Swatches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offsets_and_json() {
|
||||
assert_eq!(x_to_offset(50.0, 0.0, 200.0), 0.25);
|
||||
|
||||
@@ -314,7 +314,7 @@ pub(crate) fn trace_button(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) {
|
||||
if main.on_hover_text(tl!("Trace the image with the Default preset")).clicked() {
|
||||
trace_with(app, "Default");
|
||||
}
|
||||
if let Some(p) = egui::Popup::menu(&arrow).show(|ui| preset_list(app, ui, "")).and_then(|r| r.inner) {
|
||||
if let Some(p) = egui::Popup::menu(&arrow).show(|ui| widgets::menu_scroll(ui, |ui| preset_list(app, ui, ""))).and_then(|r| r.inner) {
|
||||
trace_with(app, &p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::super::tests_long_popups::{PopupWindow, assert_last_row_scrolls_into_view, assert_short_list_does_not_scroll};
|
||||
use super::*;
|
||||
use crate::panels::library_panel;
|
||||
use egui::{Event, PointerButton, Pos2, Rect, vec2};
|
||||
@@ -41,6 +42,30 @@ fn traced_app_with_ink(ink: u8) -> VectorcraftApp {
|
||||
app
|
||||
}
|
||||
|
||||
/// The Tracing Presets arrow at the right end of the Image Trace button.
|
||||
fn tracing_presets_arrow(window: &PopupWindow) -> Pos2 {
|
||||
let label = window.text("Image Trace").expect("the Image Trace button");
|
||||
Pos2::new(TRACE_BUTTON_W - 4.0, label.center().y)
|
||||
}
|
||||
|
||||
/// The Image Trace button's list of presets grows with saved presets: with enough of them it stops
|
||||
/// inside a short window and the wheel reaches the last one. With only the built-in presets it does
|
||||
/// not scroll.
|
||||
#[test]
|
||||
fn a_long_tracing_preset_list_scrolls_inside_a_short_window() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
let draw = |app: &mut VectorcraftApp, ui: &mut Ui| trace_button(app, ui, TRACE_BUTTON_W);
|
||||
let built_in = app.session.execute("imageTrace.presets", &json!({})).unwrap();
|
||||
let name = |p: &Value| p["name"].as_str().unwrap().to_string();
|
||||
let built_in = built_in["presets"].as_array().unwrap();
|
||||
let (first, last) = (name(&built_in[0]), name(&built_in[built_in.len() - 1]));
|
||||
assert_short_list_does_not_scroll(&mut app, draw, tracing_presets_arrow, &first, &last);
|
||||
for n in 0..30 {
|
||||
app.session.execute("imageTrace.presets.save", &json!({"name": format!("Saved {n}"), "preset": "Default"})).unwrap();
|
||||
}
|
||||
assert_last_row_scrolls_into_view(&mut app, 200.0, draw, tracing_presets_arrow, &first, "Saved 29");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_image_traces_and_the_status_bar_says_it_is_slower() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
|
||||
@@ -2,6 +2,7 @@ use egui::{Event, PointerButton, Pos2, Rect, vec2};
|
||||
use serde_json::Value;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::super::tests_long_popups::{PopupWindow, assert_last_row_scrolls_into_view, fx_menu_ends};
|
||||
use super::*;
|
||||
|
||||
/// One frame of the panel with `events` → the texts painted, with their rects.
|
||||
@@ -30,6 +31,19 @@ fn click(app: &mut VectorcraftApp, ctx: &egui::Context, texts: &[(String, Rect)]
|
||||
frame(app, ctx, vec![b(false)]);
|
||||
}
|
||||
|
||||
/// The Appearance section's fx button opens the Add Effect popup, with one row per effect group,
|
||||
/// plug-in groups included: in a short window the wheel reaches its last row.
|
||||
#[test]
|
||||
fn the_fx_popup_scrolls_inside_a_short_window() {
|
||||
let (first, last) = fx_menu_ends();
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let fx = |window: &PopupWindow| window.text("fx").expect("the fx button").center();
|
||||
assert_last_row_scrolls_into_view(&mut app, 190.0, appearance_section, fx, &first, &last);
|
||||
}
|
||||
|
||||
/// One selected clip group has Edit Clipping Path and Edit Contents at the top of the panel,
|
||||
/// under its Clip Group heading and before every section, and they select its clipping path or
|
||||
/// its clipped art; one that clips nothing has Edit Clipping Path alone. A plain group, or a clip
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
//! A headless window for checking that a panel's popup with no fixed length stays inside the window.
|
||||
|
||||
use egui::{Event, PointerButton, Pos2, Rect, Ui, vec2};
|
||||
|
||||
use super::VectorcraftApp;
|
||||
|
||||
/// The frames that opening a popup takes.
|
||||
const OPEN_FRAMES: usize = 8;
|
||||
/// The frames that finishing a wheel's scrolling takes.
|
||||
const SCROLL_FRAMES: usize = 40;
|
||||
/// The window's width, wider than every popup.
|
||||
const WIDTH: f32 = 500.0;
|
||||
/// A window tall enough for every short list.
|
||||
const TALL: f32 = 900.0;
|
||||
|
||||
/// What a window draws each frame.
|
||||
type Draw<'a> = Box<dyn FnMut(&mut VectorcraftApp, &mut Ui) + 'a>;
|
||||
|
||||
/// A window `height` pt tall with its own clock, drawing `draw` each frame.
|
||||
pub(super) struct PopupWindow<'a> {
|
||||
ctx: egui::Context,
|
||||
time: f64,
|
||||
height: f32,
|
||||
draw: Draw<'a>,
|
||||
texts: Vec<(String, Rect)>,
|
||||
}
|
||||
|
||||
impl<'a> PopupWindow<'a> {
|
||||
pub(super) fn new(app: &mut VectorcraftApp, height: f32, draw: impl FnMut(&mut VectorcraftApp, &mut Ui) + 'a) -> Self {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut window = Self { ctx, time: 0.0, height, draw: Box::new(draw), texts: vec![] };
|
||||
window.frames(app, 2);
|
||||
window
|
||||
}
|
||||
|
||||
/// One frame with `events`.
|
||||
fn frame(&mut self, app: &mut VectorcraftApp, events: Vec<Event>) {
|
||||
self.time += 1.0 / 60.0;
|
||||
let screen_rect = Some(Rect::from_min_size(Pos2::ZERO, vec2(WIDTH, self.height)));
|
||||
let input = egui::RawInput { time: Some(self.time), screen_rect, events, ..Default::default() };
|
||||
let draw = &mut self.draw;
|
||||
let mut out = self.ctx.run_ui(input, |ui| draw(app, ui));
|
||||
out.textures_delta.clear();
|
||||
self.texts = crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
fn frames(&mut self, app: &mut VectorcraftApp, count: usize) {
|
||||
for _ in 0..count {
|
||||
self.frame(app, vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the last frame drew the text `wanted`: the last one drawn, so a popup's row comes
|
||||
/// before a panel's text of the same name.
|
||||
pub(super) fn text(&self, wanted: &str) -> Option<Rect> {
|
||||
self.texts.iter().rfind(|(text, _)| text.trim() == wanted).map(|(_, rect)| *rect)
|
||||
}
|
||||
|
||||
/// The rects of the last frame's clickable widgets `size` across, in the order they were drawn.
|
||||
pub(super) fn clickable(&self, size: egui::Vec2) -> Vec<Rect> {
|
||||
self.ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.layers()
|
||||
.flat_map(|(_, w)| w.iter())
|
||||
.filter(|w| w.sense.senses_click() && w.rect.size() == size)
|
||||
.map(|w| w.rect)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// A click at `at`, and the frames a popup takes to open.
|
||||
pub(super) fn click(&mut self, app: &mut VectorcraftApp, at: Pos2) {
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
self.frame(app, vec![Event::PointerMoved(at), button(true)]);
|
||||
self.frame(app, vec![button(false)]);
|
||||
self.frames(app, OPEN_FRAMES);
|
||||
}
|
||||
|
||||
/// Turns the wheel far down with the pointer at `over`, and lets the scrolling finish.
|
||||
pub(super) fn wheel_down(&mut self, app: &mut VectorcraftApp, over: Pos2) {
|
||||
let wheel = Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, -5000.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: Default::default(),
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
};
|
||||
self.frame(app, vec![Event::PointerMoved(over), wheel]);
|
||||
self.frames(app, SCROLL_FRAMES);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Add Effect popup's first and last rows with no plug-ins: the effect catalog's groups, in order.
|
||||
pub(super) fn fx_menu_ends() -> (String, String) {
|
||||
let mut groups: Vec<&str> = vec![];
|
||||
let catalog = vectorcraft_effects::effect_catalog();
|
||||
for group in catalog.iter().map(|effect| effect.menu.get(1).copied().unwrap_or("Other")) {
|
||||
if !groups.contains(&group) {
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
(groups[0].to_string(), groups[groups.len() - 1].to_string())
|
||||
}
|
||||
|
||||
/// In a window `height` pt tall, the popup of the button `pick` finds opens at its row `first`, and
|
||||
/// the wheel brings its row `last` into the window.
|
||||
pub(super) fn assert_last_row_scrolls_into_view(
|
||||
app: &mut VectorcraftApp,
|
||||
height: f32,
|
||||
draw: impl FnMut(&mut VectorcraftApp, &mut Ui),
|
||||
pick: impl FnOnce(&PopupWindow) -> Pos2,
|
||||
first: &str,
|
||||
last: &str,
|
||||
) {
|
||||
let mut window = PopupWindow::new(app, height, draw);
|
||||
let button = pick(&window);
|
||||
window.click(app, button);
|
||||
let over = window.text(first).unwrap_or_else(|| panic!("the popup's first row, {first}")).center();
|
||||
assert!(window.text(last).is_none_or(|row| row.bottom() > height), "the popup opens at the top of its list, with {last} out of view");
|
||||
window.wheel_down(app, over);
|
||||
let row = window.text(last).unwrap_or_else(|| panic!("the wheel brings {last} into the {height} pt window"));
|
||||
assert!(row.bottom() <= height, "{last} at {row:?} is inside the {height} pt window");
|
||||
}
|
||||
|
||||
/// In a tall window the popup of the button `pick` finds shows its rows `first` to `last` without
|
||||
/// scrolling, and the wheel moves neither.
|
||||
pub(super) fn assert_short_list_does_not_scroll(
|
||||
app: &mut VectorcraftApp,
|
||||
draw: impl FnMut(&mut VectorcraftApp, &mut Ui),
|
||||
pick: impl FnOnce(&PopupWindow) -> Pos2,
|
||||
first: &str,
|
||||
last: &str,
|
||||
) {
|
||||
let mut window = PopupWindow::new(app, TALL, draw);
|
||||
let button = pick(&window);
|
||||
window.click(app, button);
|
||||
let shown = |window: &PopupWindow| {
|
||||
let (top, end) = (window.text(first).expect("the first row"), window.text(last).expect("the last row"));
|
||||
assert!(end.bottom() <= TALL, "{last} at {end:?} is inside the window");
|
||||
(top, end)
|
||||
};
|
||||
let before = shown(&window);
|
||||
window.wheel_down(app, before.0.center());
|
||||
assert_eq!(shown(&window), before, "a list that fits does not scroll");
|
||||
}
|
||||
@@ -1152,6 +1152,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- The [About brushes](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/about-brushes.html) page describes up to five Pattern brush tiles: sides, inner corner, outer corner, beginning and end. The archived [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page says to add pattern tiles to the Swatches panel and select a swatch from the scroll list for each tile. Pattern Brush Options offers document pattern swatches in all five tile menus, including swatches named like built-in choices. The brush copies each swatch's whole art and tile frame. Side requires art; explicit None on a corner prevents fallback to the other corner's art. After a swatch is deleted, the brush keeps drawing its copied art and shows it as Original. The cited pages do not describe copying the whole art, requiring Side, suppressing corner fallback after explicit None, or showing deleted swatch art as Original. `brush.options` accepts swatch names and an object form for names that match built-in choices, and rejects unknown patterns before editing. Evidence: `tests::brushsym::pattern_tile_choices_copy_whole_swatch_art_and_reject_invalid_side` and `tests::brushsym::pattern_tile_object_selects_reserved_swatch_names` (vectorcraft-engine), `tests::pattern_corner_source_none_leaves_that_kind_of_corner_empty` (vectorcraft-brush), and `dialogs::brush_options::tests::pattern_tile_menus_use_document_patterns_and_side_none_keeps_dialog_open`, `dialogs::brush_options::tests::pattern_options_borrowed_swatch_corner_keeps_its_frame`, and `dialogs::brush_options::tests::pattern_options_selects_swatches_with_reserved_names` (vectorcraft-ui-egui).
|
||||
- **Appearance:** the Appearance panel's selected row drives every paint, stroke, gradient and transparency edit (`item` on the commands); effects apply to one fill or stroke; per-item Opacity popups, whose blend-mode list and opacity slider stay open inside them (#430; the Control bar's Stroke popover too); Mixed Appearances, Layers target dots and basic-appearance rules. Effects can be reordered, moved between items and copied by dragging; Show All Hidden Attributes; clicking an effect edits it in place; the object thumbnail drags onto art; effects render on type, images, symbols, blends, envelopes, meshes and repeats, and per-item raster effects export. Groups, layers and type have their own fills, strokes and effects (Contents and Characters rows); the Layers panel's target circles target layers, groups and objects; Expand Appearance turns fills, strokes, effects and brushes into objects.
|
||||
Fill and stroke paint (Fill, Stroke, None, Apply Last Color and Apply Last Gradient, also on characters selected with the Type tool), the Stroke panel's options, gradient and freeform point edits, Swap Fill and Stroke, Default Fill and Stroke, the Eyedropper, and the Appearance panel's item edits, Clear Appearance, Reduce to Basic Appearance, Remove Item, item reordering and Move Appearance record no undo step when they change nothing.
|
||||
The Add New Effect popup (Appearance panel, Properties), the Appearance panel's swatch picker, the Gradient panel's gradient swatches menu and the Image Trace button's list of presets stop at the bottom of a short window, and the mouse wheel or the scroll bar reaches their last rows. A list that fits does not scroll (`a_long_swatch_picker_scrolls_inside_a_short_window`).
|
||||
An image's own fills and strokes draw the shape that a Convert to Shape or Outline Object effect on them gives them (the image's box or its frame, reshaped by the effects after it), under or over the image as its Contents row says, on the canvas, in exports and through Expand Appearance; new fills and strokes on an image go under it. Without one of those effects they draw nothing, and Use Preview Bounds measures an image by its frame.
|
||||
A geometry effect on one of type's own fills or strokes reshapes the whole outline that the fill or stroke paints, on the canvas and in exports; PDF, SVG, EPS and the other vector exports write such type as paths. On type that holds inline graphics, those effects are drawn and exported only when the type has a geometry effect or perspective of its own.
|
||||
The Appearance panel's inline effect editor labels each parameter as the effect dialogs' generic fields do, without the colon ("Max. Radius", "Channel 1", "Create New Fields by"). Roughen's and Zig Zag's Points, Offset Path's Joins, Inner Glow's Source and the menus of Radial Blur, Smart Blur, Sprayed Strokes, Glass, Mezzotint, Halftone Pattern, Grain, Texturizer and De-Interlace are dropdowns with the dialog's labels that send the dialog's values, and a plug-in's choice is a dropdown of the plug-in's values.
|
||||
|
||||
Reference in New Issue
Block a user