mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3704 from storytold/land/3698
Regression tests for ten fixes merged without one
This commit is contained in:
@@ -261,6 +261,32 @@ mod archive {
|
||||
assert!(matches!(a.read("doc.dat"), Err(Error::Limit(_))));
|
||||
}
|
||||
|
||||
// Regression test for #1710: ruzstd 0.9.0 let a block decompress to more than its frame's
|
||||
// window allows (RUSTSEC-2026-0344), so a crafted entry made the decoder produce far more than
|
||||
// the format permits for a block. 0.9.1 refuses it.
|
||||
#[test]
|
||||
fn a_zstd_block_larger_than_its_frames_window_is_refused() {
|
||||
const LEN: u32 = 100_000;
|
||||
let plain = vec![0x61u8; LEN as usize];
|
||||
let file = synth::container(&[("doc.dat", &plain, Method::Zstd)], None);
|
||||
// A Zstandard frame with a 1 KiB window (descriptor byte 0, window byte 0) holding one RLE
|
||||
// block, the last, that repeats a byte LEN times: block header = last | RLE << 1 | LEN << 3.
|
||||
let mut frame = vec![0x28, 0xB5, 0x2F, 0xFD, 0x00, 0x00];
|
||||
frame.extend((1 | (1 << 1) | (LEN << 3)).to_le_bytes().get(..3).unwrap());
|
||||
frame.push(0x61);
|
||||
// Swap it in for the entry's compressed bytes (they follow the "#Fil" tag at offset 72).
|
||||
let fat = u64::from_le_bytes(file[16..24].try_into().unwrap()) as usize;
|
||||
let old_len = u64::from_le_bytes(file[fat + 59 + 21..fat + 59 + 29].try_into().unwrap()) as usize;
|
||||
let mut swapped = file[..76].to_vec();
|
||||
swapped.extend(&frame);
|
||||
swapped.extend(&file[76 + old_len..]);
|
||||
let new_fat = fat - old_len + frame.len();
|
||||
swapped[16..24].copy_from_slice(&(new_fat as u64).to_le_bytes());
|
||||
swapped[new_fat + 59 + 21..new_fat + 59 + 29].copy_from_slice(&(frame.len() as u64).to_le_bytes());
|
||||
let mut a = Archive::open(&swapped, Limits::default()).unwrap();
|
||||
assert!(a.read("doc.dat").is_err(), "a block of {LEN} bytes was decompressed from a frame with a 1 KiB window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_declared_size_smaller_than_the_data_is_rejected_not_truncated() {
|
||||
let big = vec![7u8; 1 << 20];
|
||||
|
||||
@@ -4132,6 +4132,37 @@ mod tests {
|
||||
assert_eq!(edges(&mut app), active(2), "Artboard 3 is the active one of the two selected");
|
||||
}
|
||||
|
||||
// Regression test for #824: with the Artboard tool, the active artboard's name was drawn twice,
|
||||
// once by the canvas as for every artboard and once, a little higher and in blue, by the tool.
|
||||
// Now each artboard is named once, and the active one's name sits where the others' do.
|
||||
#[test]
|
||||
fn the_artboard_tool_names_its_active_artboard_once() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let drawn = shapes(&mut app, &ctx);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
// Each name drawn: how far its left edge is right of the artboard's corner, and its bottom above it.
|
||||
let named = |name: &str, board: usize| -> Vec<(f32, f32)> {
|
||||
let r = app.session.active().unwrap().doc.artboards[board].rect;
|
||||
let corner = xf.to_screen(Point::new(r.x0, r.y0));
|
||||
drawn
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
Shape::Text(t) if t.galley.text() == name => Some((t.pos.x - corner.x, corner.y - (t.pos.y + t.galley.size().y))),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let (active, other) = (named("01 - Artboard 1", 0), named("02 - Artboard 2", 1));
|
||||
assert_eq!(other.len(), 1, "the other artboard is named once: {other:?}");
|
||||
assert_eq!(active.len(), 1, "the active artboard is named once: {active:?}");
|
||||
assert!((active[0].0 - other[0].0).abs() < 0.6, "flush with the artboard's left edge like the other name: {active:?} vs {other:?}");
|
||||
assert!((active[0].1 - other[0].1).abs() < 2.0, "on the artboard's top edge like the other name: {active:?} vs {other:?}");
|
||||
}
|
||||
|
||||
/// Enable Touch Gestures (#585): a two-finger tap undoes and a three-finger one redoes, and the
|
||||
/// first finger's press (the pointer egui makes of it) draws nothing. Off, a tap does nothing.
|
||||
#[test]
|
||||
|
||||
@@ -894,3 +894,38 @@ mod typed_number_tests {
|
||||
assert!(typed_number(1e20).is_f64());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod selected_text_tests {
|
||||
use super::*;
|
||||
use crate::widgets::look_tests::{frame, glyph_colours, themed};
|
||||
|
||||
// Regression test for #1465: selected text in a dialog's text field was in the accent, on a
|
||||
// selection tinted with the same accent. It is in the strong text colour.
|
||||
#[test]
|
||||
fn selected_text_in_a_dialog_text_field_is_in_the_strong_text_colour() {
|
||||
for brightness in crate::theme::Brightness::ALL {
|
||||
let ctx = themed(brightness);
|
||||
let t = Tokens::get(&ctx);
|
||||
let mut d = Dialog::new("test", json!({"name": "Artboard 1"}));
|
||||
let id = std::cell::Cell::new(egui::Id::NULL);
|
||||
let mut add = |ui: &mut egui::Ui| {
|
||||
id.set(ui.id().with(("dlg-text", "name")));
|
||||
text_edit(ui, &mut d, "name", 120.0);
|
||||
};
|
||||
frame(&ctx, 0.0, vec![], &mut add);
|
||||
// Focus the field and select all of its text.
|
||||
ctx.memory_mut(|m| m.request_focus(id.get()));
|
||||
frame(&ctx, 0.1, vec![], &mut add);
|
||||
let mut state = egui::TextEdit::load_state(&ctx, id.get()).unwrap_or_default();
|
||||
state.cursor.set_char_range(Some(egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(10))));
|
||||
state.store(&ctx, id.get());
|
||||
let shapes = frame(&ctx, 0.2, vec![], &mut add);
|
||||
let colours = glyph_colours(&shapes);
|
||||
assert!(!colours.is_empty(), "{brightness:?}: the text is drawn");
|
||||
let mut used = colours.clone();
|
||||
used.dedup();
|
||||
assert_eq!(used, [t.text_strong], "{brightness:?}: selected text in {used:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,31 @@ mod tests {
|
||||
assert!(appearance.contains("<circle") && !pathfinder.contains("<circle"), "appearance should read as a list, not overlapping squares");
|
||||
}
|
||||
|
||||
// Regression test for #479: the Add and Delete Anchor Point tools' icons were drawn apart from
|
||||
// the Pen tool's, so the three tools didn't read as one family. They are the Pen's pen with a
|
||||
// plus or a minus beside it.
|
||||
#[test]
|
||||
fn the_add_and_delete_anchor_tool_icons_are_the_pen_icon_with_a_badge() {
|
||||
let svg = |tool: &str| {
|
||||
let name = tool_icon(tool);
|
||||
let (_, bytes) =
|
||||
crate::icon_data::ICONS.iter().find(|(n, _)| *n == name).unwrap_or_else(|| panic!("{tool} maps to {name}, which isn't an icon"));
|
||||
String::from_utf8_lossy(bytes).into_owned()
|
||||
};
|
||||
let shapes =
|
||||
|svg: &str| svg.lines().map(|l| l.trim().to_string()).filter(|l| l.starts_with("<path") || l.starts_with("<circle")).collect::<Vec<_>>();
|
||||
let pen = shapes(&svg("tool-pen"));
|
||||
assert!(!pen.is_empty(), "the Pen's icon has shapes");
|
||||
let (add, delete) = (shapes(&svg("tool-pen-add")), shapes(&svg("tool-pen-delete")));
|
||||
for (tool, icon) in [("tool-pen-add", &add), ("tool-pen-delete", &delete)] {
|
||||
for shape in &pen {
|
||||
assert!(icon.contains(shape), "{tool}'s icon lacks the Pen's {shape}");
|
||||
}
|
||||
assert!(icon.len() > pen.len(), "{tool}'s icon has a badge of its own");
|
||||
}
|
||||
assert_ne!(add, delete, "plus and minus differ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_catalog_preserves_fallback_and_tool_aliases() {
|
||||
let ImageSource::Bytes { uri, bytes } = source("missing-icon") else { panic!("expected SVG bytes") };
|
||||
|
||||
@@ -802,6 +802,25 @@ mod tests {
|
||||
assert_eq!(selected(&app), std::slice::from_ref(&mask), "Edit Clipping Path selects the clipping path");
|
||||
}
|
||||
|
||||
// Regression test for #1910: the Properties panel's flip buttons are named as the reference
|
||||
// app's Properties panel names them, not Flip Horizontal and Flip Vertical (the Transform
|
||||
// panel menu's names).
|
||||
#[test]
|
||||
fn the_flip_buttons_keep_the_reference_properties_names() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 50, "y": 50, "width": 120, "height": 80})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let tree = crate::widgets::accessibility_tests::tree(|ui| transform_section(&mut app, ui));
|
||||
let names: Vec<&str> = tree.iter().filter_map(|n| n.label()).collect();
|
||||
for want in ["Flip Along Horizontal Axis", "Flip Along Vertical Axis"] {
|
||||
assert!(names.contains(&want), "{want} in {names:?}");
|
||||
}
|
||||
for old in ["Flip Horizontal", "Flip Vertical"] {
|
||||
assert!(!names.contains(&old), "{old} in {names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #3451: the Properties panel Transform section shows ellipse pie angles and Invert Pie.
|
||||
#[test]
|
||||
fn transform_section_shows_ellipse_pie_properties() {
|
||||
|
||||
@@ -2970,3 +2970,246 @@ mod button_tests {
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// How the shared widgets look once laid out and painted: shapes of whole frames, measured.
|
||||
#[cfg(test)]
|
||||
pub(crate) mod look_tests {
|
||||
use super::*;
|
||||
|
||||
/// A context with the fonts and the theme in `brightness`, in English.
|
||||
pub(crate) fn themed(brightness: theme::Brightness) -> egui::Context {
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
theme::apply(&ctx, brightness);
|
||||
ctx
|
||||
}
|
||||
|
||||
/// One frame at `time` seconds of `add` in a 600 × 400 window with `events` → the shapes it
|
||||
/// drew, `Shape::Vec`s flattened.
|
||||
pub(crate) fn frame(ctx: &egui::Context, time: f64, events: Vec<egui::Event>, add: impl FnMut(&mut Ui)) -> Vec<egui::Shape> {
|
||||
fn flat(s: egui::Shape, out: &mut Vec<egui::Shape>) {
|
||||
match s {
|
||||
egui::Shape::Vec(v) => v.into_iter().for_each(|s| flat(s, out)),
|
||||
s => out.push(s),
|
||||
}
|
||||
}
|
||||
let raw =
|
||||
egui::RawInput { time: Some(time), screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(600.0, 400.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, add);
|
||||
out.textures_delta.clear();
|
||||
let mut shapes = vec![];
|
||||
out.shapes.into_iter().for_each(|c| flat(c.shape, &mut shapes));
|
||||
shapes
|
||||
}
|
||||
|
||||
// Regression test for #808: the scroll bar of the toolbar and the dock's icon column widened to
|
||||
// 10 points while it was dragged or the pointer was on it, covering the group labels and the
|
||||
// right edge of the tool icons. It stays a slim line.
|
||||
#[test]
|
||||
fn the_strip_scroll_bar_stays_a_slim_line_while_the_pointer_is_on_it() {
|
||||
let ctx = themed(Default::default());
|
||||
let area = std::cell::Cell::new(Rect::NOTHING);
|
||||
let mut add = |ui: &mut Ui| {
|
||||
area.set(
|
||||
ui.allocate_ui(vec2(80.0, 200.0), |ui| {
|
||||
strip_scroll(ui, "strip", |ui| {
|
||||
for i in 0..40 {
|
||||
ui.label(format!("Tool {i}"));
|
||||
}
|
||||
});
|
||||
})
|
||||
.response
|
||||
.rect,
|
||||
);
|
||||
};
|
||||
frame(&ctx, 0.0, vec![], &mut add);
|
||||
let area = area.get();
|
||||
assert!(area.height() >= 199.0, "the strip fills the height it was given: {area:?}");
|
||||
// The pointer comes onto the bar's edge and stays there while the bar settles.
|
||||
let on_the_bar = egui::Event::PointerMoved(pos2(area.right() - 1.0, area.center().y));
|
||||
let mut widest = 0.0_f32;
|
||||
for i in 1..=15 {
|
||||
let shapes = frame(&ctx, f64::from(i) * 0.1, if i == 1 { vec![on_the_bar.clone()] } else { vec![] }, &mut add);
|
||||
for s in shapes {
|
||||
// The bar's track and handle are the filled rectangles.
|
||||
if let egui::Shape::Rect(r) = s
|
||||
&& r.fill != Color32::TRANSPARENT
|
||||
{
|
||||
widest = widest.max(r.rect.width());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(widest > 0.0, "the scroll bar is drawn");
|
||||
assert!(widest <= 4.5, "the scroll bar is {widest} points wide");
|
||||
}
|
||||
// Regression test for #900: a number field with a spinner and a preset list drew as three
|
||||
// boxes side by side: its text box was a point taller than the 26-point row, and each joint had
|
||||
// two borders.
|
||||
#[test]
|
||||
fn a_spinner_field_reads_as_one_box_in_three_parts() {
|
||||
let ctx = themed(Default::default());
|
||||
let mut add = |ui: &mut Ui| {
|
||||
ui.horizontal(|ui| spin_field(ui, "weight", Some(4.0), Unit::Points, 100.0, 1.0, 0.0, &[1.0, 2.0]));
|
||||
};
|
||||
frame(&ctx, 0.0, vec![], &mut add);
|
||||
let shapes = frame(&ctx, 0.1, vec![], &mut add);
|
||||
// The boxes of the row: the spinner, the field and the preset chevron, left to right.
|
||||
let mut boxes: Vec<Rect> = vec![];
|
||||
for s in &shapes {
|
||||
if let egui::Shape::Rect(r) = s
|
||||
&& r.rect.height() > 20.0
|
||||
&& !boxes.contains(&r.rect)
|
||||
{
|
||||
boxes.push(r.rect);
|
||||
}
|
||||
}
|
||||
boxes.sort_by(|a, b| a.min.x.total_cmp(&b.min.x));
|
||||
assert_eq!(boxes.len(), 3, "{boxes:?}");
|
||||
for b in &boxes {
|
||||
assert_eq!((b.min.y, b.height()), (boxes[0].min.y, 26.0), "all as tall as the row: {boxes:?}");
|
||||
}
|
||||
for pair in boxes.windows(2) {
|
||||
assert!((pair[1].min.x - pair[0].max.x + 1.0).abs() < 0.01, "neighbours share one border: {boxes:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour of every glyph of the text in `shapes`: selected glyphs are drawn in the
|
||||
/// selection stroke's colour.
|
||||
pub(crate) fn glyph_colours(shapes: &[egui::Shape]) -> Vec<Color32> {
|
||||
let mut colours = vec![];
|
||||
for s in shapes {
|
||||
if let egui::Shape::Text(t) = s {
|
||||
for row in &t.galley.rows {
|
||||
let visuals = &row.row.visuals;
|
||||
colours.extend(visuals.mesh.vertices[visuals.glyph_vertex_range.clone()].iter().map(|v| v.color));
|
||||
}
|
||||
}
|
||||
}
|
||||
colours
|
||||
}
|
||||
|
||||
// Regression test for #1465: the selected number in a value field was hard to read in every
|
||||
// brightness: egui draws selected text in the selection stroke's colour, the accent, on a
|
||||
// selection tinted with the same accent. It is in the strong text colour.
|
||||
#[test]
|
||||
fn selected_text_in_a_value_field_is_in_the_strong_text_colour() {
|
||||
for brightness in theme::Brightness::ALL {
|
||||
let ctx = themed(brightness);
|
||||
let t = Tokens::get(&ctx);
|
||||
let id = std::cell::Cell::new(egui::Id::NULL);
|
||||
let mut add = |ui: &mut Ui| {
|
||||
id.set(ui.id().with("w"));
|
||||
num_field(ui, "w", Some(1234.0), Unit::Points, 80.0);
|
||||
};
|
||||
frame(&ctx, 0.0, vec![], &mut add);
|
||||
// The field takes focus, and all of its text is selected.
|
||||
ctx.memory_mut(|m| m.request_focus(id.get()));
|
||||
frame(&ctx, 0.1, vec![], &mut add);
|
||||
assert!(ctx.memory(|m| m.has_focus(id.get())), "{brightness:?}: the field has focus");
|
||||
let len = Unit::Points.format(1234.0).chars().count();
|
||||
let mut state = egui::TextEdit::load_state(&ctx, id.get()).unwrap_or_default();
|
||||
state.cursor.set_char_range(Some(egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(len))));
|
||||
state.store(&ctx, id.get());
|
||||
let shapes = frame(&ctx, 0.2, vec![], &mut add);
|
||||
let colours = glyph_colours(&shapes);
|
||||
assert!(!colours.is_empty(), "{brightness:?}: the number is drawn");
|
||||
let mut used = colours.clone();
|
||||
used.dedup();
|
||||
assert_eq!(used, [t.text_strong], "{brightness:?}: selected text in {used:?}");
|
||||
assert_ne!(t.text_strong, t.accent);
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for #1483: the theme's widget fill is the panel's colour, so a slider in a
|
||||
// dialog showed only its handle. `slider` draws the rail in the buttons' border colour and
|
||||
// fills it up to the handle in the accent.
|
||||
#[test]
|
||||
fn a_slider_shows_its_rail_filled_up_to_the_handle() {
|
||||
for brightness in theme::Brightness::ALL {
|
||||
let ctx = themed(brightness);
|
||||
let t = Tokens::get(&ctx);
|
||||
let mut value = 30.0_f64;
|
||||
let mut add = |ui: &mut Ui| {
|
||||
slider(ui, egui::Slider::new(&mut value, 0.0..=100.0).show_value(false));
|
||||
};
|
||||
frame(&ctx, 0.0, vec![], &mut add);
|
||||
let shapes = frame(&ctx, 0.1, vec![], &mut add);
|
||||
let rects: Vec<(Rect, Color32)> =
|
||||
shapes.iter().filter_map(|s| if let egui::Shape::Rect(r) = s { Some((r.rect, r.fill)) } else { None }).collect();
|
||||
let rail = rects.iter().find(|(_, c)| *c == t.button_border).map(|(r, _)| *r);
|
||||
let filled = rects.iter().find(|(_, c)| *c == t.accent).map(|(r, _)| *r);
|
||||
let (Some(rail), Some(filled)) = (rail, filled) else {
|
||||
panic!("{brightness:?}: the rail ({:?}) and its fill ({:?}) are drawn: {rects:?}", t.button_border, t.accent);
|
||||
};
|
||||
assert!(filled.width() > 1.0 && filled.width() < rail.width(), "{brightness:?}: filled {filled:?} of the rail {rail:?}");
|
||||
assert!((filled.min.x - rail.min.x).abs() < 1.0, "{brightness:?}: the fill starts at the rail's start");
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for #2083: the rows of the panels' menus didn't light up under the pointer, as
|
||||
// the menu bar's items do.
|
||||
#[test]
|
||||
fn a_panel_menu_row_lights_up_under_the_pointer() {
|
||||
let ctx = themed(Default::default());
|
||||
let row_at = std::cell::Cell::new(Rect::NOTHING);
|
||||
let mut add = |ui: &mut Ui| {
|
||||
row_at.set(
|
||||
ui.scope(|ui| {
|
||||
menu_item_name(ui, "Rename", true, false);
|
||||
})
|
||||
.response
|
||||
.rect,
|
||||
);
|
||||
};
|
||||
// A filled rectangle behind the row's text.
|
||||
let lit = |shapes: &[egui::Shape], row: Rect| {
|
||||
shapes.iter().any(|s| matches!(s, egui::Shape::Rect(r) if r.fill != Color32::TRANSPARENT && r.rect.contains(row.center())))
|
||||
};
|
||||
frame(&ctx, 0.0, vec![], &mut add);
|
||||
let row = row_at.get();
|
||||
let at_rest = frame(&ctx, 0.1, vec![], &mut add);
|
||||
assert!(!lit(&at_rest, row), "no frame at rest");
|
||||
frame(&ctx, 0.2, vec![egui::Event::PointerMoved(row.center())], &mut add);
|
||||
let hovered = frame(&ctx, 0.3, vec![], &mut add);
|
||||
assert!(lit(&hovered, row), "the row under the pointer is lit: {row:?}");
|
||||
}
|
||||
|
||||
// Regression test for #3440: the Fill/Stroke chip's popover closed on the click that ended a
|
||||
// drag between its proxies. It stays open for a click on its own anchor and while a panel
|
||||
// drag is on; a click elsewhere still closes it.
|
||||
#[test]
|
||||
fn a_popover_stays_open_for_a_click_on_its_anchor_or_during_a_panel_drag() {
|
||||
let press = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
// Opens the popover, then clicks at `click` (with `drag` as the panel drag under way).
|
||||
let open_after_click = |click: Option<Pos2>, drag: Option<PanelDrag>| -> bool {
|
||||
let ctx = themed(Default::default());
|
||||
let toggle = std::cell::Cell::new(true);
|
||||
let shown = std::cell::Cell::new(false);
|
||||
let anchor = std::cell::Cell::new(Rect::NOTHING);
|
||||
let mut add = |ui: &mut Ui| {
|
||||
let resp = ui.allocate_response(vec2(40.0, 24.0), Sense::hover());
|
||||
anchor.set(resp.rect);
|
||||
shown.set(popover(&resp, toggle.replace(false), |ui| ui.label("Colour")).is_some());
|
||||
};
|
||||
frame(&ctx, 0.0, vec![], &mut add);
|
||||
assert!(shown.get(), "the toggle opens it");
|
||||
frame(&ctx, 0.2, vec![], &mut add);
|
||||
assert!(shown.get(), "it stays open");
|
||||
if let Some(d) = drag {
|
||||
egui::DragAndDrop::set_payload(&ctx, d);
|
||||
}
|
||||
let pos = click.unwrap_or_else(|| anchor.get().center());
|
||||
frame(&ctx, 0.3, vec![egui::Event::PointerMoved(pos), press(pos, true)], &mut add);
|
||||
frame(&ctx, 0.4, vec![press(pos, false)], &mut add);
|
||||
frame(&ctx, 0.5, vec![], &mut add);
|
||||
shown.get()
|
||||
};
|
||||
let far = Some(pos2(500.0, 350.0));
|
||||
let panel_drag = || Some(PanelDrag::Symbol("s".into()));
|
||||
// Whether it is open after: a click elsewhere; a click on its anchor that doesn't toggle it;
|
||||
// a panel drag released elsewhere.
|
||||
let open = [open_after_click(far, None), open_after_click(None, None), open_after_click(far, panel_drag())];
|
||||
assert_eq!(open, [false, true, true]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user