mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-22 07:04:10 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4167681bb1 |
No files matched your search
@@ -42,6 +42,89 @@ pub struct Drag {
|
||||
pub erase: bool,
|
||||
/// ⇧ constraint of a painting stroke (stroke_constraint.rs).
|
||||
pub constrain: Option<crate::stroke_constraint::Axis>,
|
||||
/// Modifiers held now (updated on every pointer event of the gesture).
|
||||
pub live: egui::Modifiers,
|
||||
/// Which of the modifiers held at the press have been released since.
|
||||
pub released: egui::Modifiers,
|
||||
}
|
||||
|
||||
impl Drag {
|
||||
pub fn new(tool: Tool, start: [f64; 2], points: Vec<[f64; 3]>, modifiers: egui::Modifiers, erase: bool) -> Self {
|
||||
Self { tool, start, points, modifiers, erase, constrain: None, live: modifiers, released: egui::Modifiers::NONE }
|
||||
}
|
||||
|
||||
/// Record the modifiers of a pointer event.
|
||||
fn track(&mut self, mods: egui::Modifiers) {
|
||||
self.live = mods;
|
||||
self.released.shift |= !mods.shift;
|
||||
self.released.alt |= !mods.alt;
|
||||
}
|
||||
|
||||
/// ⇧ (square / circle) and ⌥ (from the centre) for a marquee drag. A modifier held at the press
|
||||
/// picks the selection mode instead (add / subtract, #188) until it is released and pressed
|
||||
/// again, as in Photoshop.
|
||||
pub fn marquee_mods(&self) -> (bool, bool) {
|
||||
let fresh = |now: bool, at_press: bool, released: bool| now && (!at_press || released);
|
||||
(fresh(self.live.shift, self.modifiers.shift, self.released.shift), fresh(self.live.alt, self.modifiers.alt, self.released.alt))
|
||||
}
|
||||
}
|
||||
|
||||
/// The marquee dragged from `d.start` to `end` as two document-space corners: the options-bar
|
||||
/// style (fixed ratio / fixed size), ⇧ for a square or circle, ⌥ to draw from the centre.
|
||||
pub fn marquee_corners(o: &crate::state::ToolOptions, d: &Drag, end: [f64; 2]) -> ([f64; 2], [f64; 2]) {
|
||||
let (shift, alt) = d.marquee_mods();
|
||||
let e = crate::chrome_ui::marquee_end(&o.marquee_style, o.marquee_width as f64, o.marquee_height as f64, shift, d.start, end);
|
||||
if !alt {
|
||||
return (d.start, e);
|
||||
}
|
||||
let (dx, dy) = (e[0] - d.start[0], e[1] - d.start[1]);
|
||||
// A fixed size is centred on the press point; otherwise the drag is the half extent.
|
||||
let (hx, hy) = if o.marquee_style == "fixedSize" { (dx / 2.0, dy / 2.0) } else { (dx, dy) };
|
||||
([d.start[0] - hx, d.start[1] - hy], [d.start[0] + hx, d.start[1] + hy])
|
||||
}
|
||||
|
||||
/// The pixel rectangle `[x0, y0, x1, y1]` a marquee between two corners selects.
|
||||
pub fn marquee_px(a: [f64; 2], b: [f64; 2]) -> [f64; 4] {
|
||||
[a[0].min(b[0]).floor(), a[1].min(b[1]).floor(), a[0].max(b[0]).ceil(), a[1].max(b[1]).ceil()]
|
||||
}
|
||||
|
||||
/// The size readout shown beside the cursor while dragging a marquee: the width and height values.
|
||||
pub fn marquee_readout(r: [f64; 4]) -> [String; 2] {
|
||||
[format!("{} px", r[2] - r[0]), format!("{} px", r[3] - r[1])]
|
||||
}
|
||||
|
||||
/// Draw the marquee size readout below-right of the cursor (kept on screen), like Photoshop's:
|
||||
/// two rows, `W:` / `H:` labels on the left and the values right-aligned.
|
||||
fn draw_marquee_readout(ctx: &egui::Context, cursor: Pos2, values: [String; 2]) {
|
||||
let t = crate::theme::Tokens::get(ctx);
|
||||
let font = egui::FontId::proportional(11.5);
|
||||
let labels = ["W:", "H:"];
|
||||
let width = |text: &str| ctx.fonts_mut(|f| f.layout_no_wrap(text.to_owned(), font.clone(), t.text).size().x);
|
||||
let (lw, vw) = (labels.map(width), [width(&values[0]), width(&values[1])]);
|
||||
let (label_col, value_col) = (lw[0].max(lw[1]), vw[0].max(vw[1]));
|
||||
egui::Area::new(egui::Id::new("marquee-readout"))
|
||||
.order(egui::Order::Tooltip)
|
||||
.fixed_pos(cursor + vec2(16.0, 18.0))
|
||||
.interactable(false)
|
||||
.constrain(true)
|
||||
.show(ctx, |ui| {
|
||||
egui::Frame::new()
|
||||
.fill(t.card)
|
||||
.stroke(Stroke::new(1.0, t.card_border))
|
||||
.corner_radius(t.radius_sm)
|
||||
.inner_margin(egui::Margin::symmetric(7, 4))
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 1.0);
|
||||
for (i, value) in values.into_iter().enumerate() {
|
||||
// Labels left, values right-aligned on one edge, sharing a baseline.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::Label::new(egui::RichText::new(labels[i]).font(font.clone()).color(t.text_dim)).extend());
|
||||
ui.add_space(label_col - lw[i] + 12.0 + value_col - vw[i]);
|
||||
ui.add(egui::Label::new(egui::RichText::new(value).font(font.clone()).color(t.text)).extend());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// A Brush/Eraser stroke shown while it is drawn: the engine renders the real dabs onto a copy of
|
||||
@@ -1594,10 +1677,10 @@ fn draw_drag_preview(app: &mut PhotocraftApp, painter: &egui::Painter, xf: &View
|
||||
app.trail = None;
|
||||
return;
|
||||
};
|
||||
let mut last = d.points.last().map(|p| [p[0], p[1]]).unwrap_or(d.start);
|
||||
if matches!(d.tool, Tool::RectMarquee | Tool::EllipseMarquee) {
|
||||
let o = &app.ui.tool_options;
|
||||
last = crate::chrome_ui::marquee_end(&o.marquee_style, o.marquee_width as f64, o.marquee_height as f64, false, d.start, last);
|
||||
let last = d.points.last().map(|p| [p[0], p[1]]).unwrap_or(d.start);
|
||||
let marquee = matches!(d.tool, Tool::RectMarquee | Tool::EllipseMarquee).then(|| marquee_corners(&app.ui.tool_options, d, last));
|
||||
if let Some((a, b)) = marquee {
|
||||
draw_marquee_readout(painter.ctx(), xf.to_screen(last[0] as f32, last[1] as f32), marquee_readout(marquee_px(a, b)));
|
||||
}
|
||||
match d.tool {
|
||||
// The canvas shows the live stroke itself (`LiveStroke`).
|
||||
@@ -1616,7 +1699,8 @@ fn draw_drag_preview(app: &mut PhotocraftApp, painter: &egui::Painter, xf: &View
|
||||
t if crate::vector_ui::is_shape_tool(t) => crate::vector_ui::draw_shape_preview(app, painter, xf, t, d.start, last, d.modifiers),
|
||||
Tool::RectMarquee | Tool::EllipseMarquee | Tool::ObjectSelection => {
|
||||
// Marching ants, visible on any pixels (#172).
|
||||
let r = Rect::from_two_pos(xf.to_screen(d.start[0] as f32, d.start[1] as f32), xf.to_screen(last[0] as f32, last[1] as f32));
|
||||
let (a, b) = marquee.unwrap_or((d.start, last));
|
||||
let r = Rect::from_two_pos(xf.to_screen(a[0] as f32, a[1] as f32), xf.to_screen(b[0] as f32, b[1] as f32));
|
||||
let r = Rect::from_min_max(r.min.round() + vec2(0.5, 0.5), r.max.round() + vec2(0.5, 0.5));
|
||||
let pts = if d.tool == Tool::EllipseMarquee {
|
||||
crate::tool_feedback::ellipse_points(r)
|
||||
@@ -1784,7 +1868,7 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers)
|
||||
if let Some(p) = from {
|
||||
points.insert(0, [p[0], p[1], pressure as f64]);
|
||||
}
|
||||
app.drag = Some(Drag { tool, start: from.unwrap_or([x, y]), points, modifiers: mods, erase, constrain: None });
|
||||
app.drag = Some(Drag::new(tool, from.unwrap_or([x, y]), points, mods, erase));
|
||||
app.trail = None;
|
||||
app.stylus.begin_stroke();
|
||||
if from.is_some() {
|
||||
@@ -1801,6 +1885,7 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers)
|
||||
}
|
||||
let zoom = app.current_zoom();
|
||||
if let Some(d) = &mut app.drag {
|
||||
d.track(mods);
|
||||
// ⇧: straight 0/45/90° strokes, 45° gradient angles (stroke_constraint.rs).
|
||||
let last = d.points.last().map_or(d.start, |p| [p[0], p[1]]);
|
||||
let [x, y] = crate::stroke_constraint::constrain(d.tool, &mut d.constrain, d.start, last, [x, y], mods.shift, zoom);
|
||||
@@ -1822,6 +1907,7 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers)
|
||||
}
|
||||
let zoom = app.current_zoom();
|
||||
let Some(mut d) = app.drag.take() else { return };
|
||||
d.track(mods);
|
||||
let last = d.points.last().map_or(d.start, |p| [p[0], p[1]]);
|
||||
let [x, y] = crate::stroke_constraint::constrain(d.tool, &mut d.constrain, d.start, last, [x, y], mods.shift, zoom);
|
||||
// A Move-tool click (released where it was pressed) selects, it never moves: snapping
|
||||
@@ -1881,11 +1967,8 @@ fn finish_gesture(app: &mut PhotocraftApp, d: Drag) {
|
||||
}
|
||||
}
|
||||
Tool::RectMarquee | Tool::EllipseMarquee => {
|
||||
let o = &app.ui.tool_options;
|
||||
let e = crate::chrome_ui::marquee_end(&o.marquee_style, o.marquee_width as f64, o.marquee_height as f64, false, d.start, [end[0], end[1]]);
|
||||
let end = [e[0], e[1], 1.0];
|
||||
let (x0, y0) = (d.start[0].min(end[0]).floor(), d.start[1].min(end[1]).floor());
|
||||
let (x1, y1) = (d.start[0].max(end[0]).ceil(), d.start[1].max(end[1]).ceil());
|
||||
let (a, b) = marquee_corners(&app.ui.tool_options, &d, [end[0], end[1]]);
|
||||
let [x0, y0, x1, y1] = marquee_px(a, b);
|
||||
if x1 - x0 < 2.0 || y1 - y0 < 2.0 {
|
||||
if app.session.is_enabled("select.deselect") {
|
||||
let _ = app.run("select.deselect", json!({}));
|
||||
|
||||
@@ -1133,6 +1133,9 @@ impl PhotocraftApp {
|
||||
#[cfg(test)]
|
||||
mod input_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod marquee_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod clipboard_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! Marquee modifiers through the real canvas (#208): ⇧ during the drag draws a square or circle,
|
||||
//! ⌥ draws from the centre (both together work too), a "W × H px" readout follows the cursor, and
|
||||
//! modifiers held before the drag still pick add/subtract (#188) instead of constraining.
|
||||
|
||||
use egui::{Modifiers, PointerButton, Pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use photocraft_geom::Rect;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
use crate::canvas::ViewXform;
|
||||
use crate::state::Tool;
|
||||
|
||||
fn harness(tool: Tool) -> Harness<'static, PhotocraftApp> {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.sync_views();
|
||||
app.ui.extras.rulers = false;
|
||||
app.ui.tool = tool;
|
||||
let mut h = Harness::builder().with_size(vec2(1000.0, 700.0)).build_ui_state(
|
||||
|ui, app: &mut PhotocraftApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
if !ctx.fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
|
||||
return;
|
||||
}
|
||||
crate::shortcuts::handle(app, &ctx);
|
||||
egui::CentralPanel::default().show(ui, |ui| crate::canvas::document_area(app, ui));
|
||||
},
|
||||
app,
|
||||
);
|
||||
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
|
||||
h.run_steps(4);
|
||||
// 100 %: one document pixel per point, so drags land on whole pixels.
|
||||
let v = &mut h.state_mut().ui.views[0];
|
||||
v.zoom = 1.0;
|
||||
v.center = [200.0, 150.0];
|
||||
v.fit_pending = false;
|
||||
h.run_steps(2);
|
||||
h
|
||||
}
|
||||
|
||||
fn screen(h: &Harness<'static, PhotocraftApp>, x: f32, y: f32) -> Pos2 {
|
||||
let app = h.state();
|
||||
let v = &app.ui.views[0];
|
||||
let xf = ViewXform { rect: crate::rulers::content_rect(app, app.last_canvas_rect), zoom: v.zoom, center: v.center, flip: app.ui.view.flip_horizontal };
|
||||
xf.to_screen(x, y)
|
||||
}
|
||||
|
||||
fn mods(h: &mut Harness<'static, PhotocraftApp>, m: Modifiers) {
|
||||
h.event(egui::Event::ModifiersChanged(m));
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
fn button(h: &mut Harness<'static, PhotocraftApp>, p: Pos2, down: bool, m: Modifiers) {
|
||||
h.event(egui::Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: down, modifiers: m });
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
/// Move the pointer to document point `(x, y)` in a few steps.
|
||||
fn move_to(h: &mut Harness<'static, PhotocraftApp>, x: f32, y: f32) {
|
||||
let p = screen(h, x, y);
|
||||
h.event(egui::Event::PointerMoved(p));
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn press_at(h: &mut Harness<'static, PhotocraftApp>, x: f32, y: f32, m: Modifiers) {
|
||||
let p = screen(h, x, y);
|
||||
h.event(egui::Event::PointerMoved(p));
|
||||
h.run_steps(1);
|
||||
button(h, p, true, m);
|
||||
// Past egui's click distance, so the drag starts at the press point.
|
||||
move_to(h, x + 8.0, y + 6.0);
|
||||
}
|
||||
|
||||
fn release_at(h: &mut Harness<'static, PhotocraftApp>, x: f32, y: f32, m: Modifiers) {
|
||||
move_to(h, x, y);
|
||||
button(h, screen(h, x, y), false, m);
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn selection(h: &Harness<'static, PhotocraftApp>) -> Rect {
|
||||
h.state().session.active().unwrap().doc.selection.as_ref().map_or(Rect::EMPTY, |s| s.content_bounds())
|
||||
}
|
||||
|
||||
/// The two-row readout shows `W: {w} px` and `H: {ht} px` (Photoshop's format).
|
||||
fn readout(h: &Harness<'static, PhotocraftApp>, w: i32, ht: i32) -> bool {
|
||||
let labels = h.query_by_label("W:").is_some() && h.query_by_label("H:").is_some();
|
||||
let count = |v: i32| h.query_all_by_label(&format!("{v} px")).count();
|
||||
let values = if w == ht { count(w) == 2 } else { count(w) == 1 && count(ht) == 1 };
|
||||
labels && values && h.query_all_by_label_contains(" px").count() == 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_during_the_drag_draws_a_square_and_the_readout_follows() {
|
||||
let mut h = harness(Tool::RectMarquee);
|
||||
press_at(&mut h, 50.0, 50.0, Modifiers::NONE);
|
||||
move_to(&mut h, 150.0, 90.0);
|
||||
assert!(readout(&h, 100, 40), "free drag: 100 × 40");
|
||||
mods(&mut h, Modifiers::SHIFT);
|
||||
move_to(&mut h, 150.0, 91.0);
|
||||
assert!(readout(&h, 100, 100), "⇧: the larger extent wins");
|
||||
release_at(&mut h, 150.0, 91.0, Modifiers::SHIFT);
|
||||
mods(&mut h, Modifiers::NONE);
|
||||
assert_eq!(selection(&h), Rect::new(50, 50, 150, 150));
|
||||
assert!(h.query_by_label("W:").is_none() && h.query_by_label_contains(" px").is_none(), "the readout goes away with the drag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_draws_from_the_centre_and_shift_alt_a_centred_circle() {
|
||||
let mut h = harness(Tool::RectMarquee);
|
||||
press_at(&mut h, 200.0, 150.0, Modifiers::NONE);
|
||||
mods(&mut h, Modifiers::ALT);
|
||||
move_to(&mut h, 240.0, 170.0);
|
||||
assert!(readout(&h, 80, 40));
|
||||
release_at(&mut h, 240.0, 170.0, Modifiers::ALT);
|
||||
mods(&mut h, Modifiers::NONE);
|
||||
assert_eq!(selection(&h), Rect::new(160, 130, 240, 170), "centred on the press point");
|
||||
// Elliptical, ⇧⌥: a circle centred on the press point (replacing the selection).
|
||||
let mut h = harness(Tool::EllipseMarquee);
|
||||
press_at(&mut h, 200.0, 150.0, Modifiers::NONE);
|
||||
mods(&mut h, Modifiers::SHIFT | Modifiers::ALT);
|
||||
move_to(&mut h, 230.0, 160.0);
|
||||
assert!(readout(&h, 60, 60));
|
||||
release_at(&mut h, 230.0, 160.0, Modifiers::SHIFT | Modifiers::ALT);
|
||||
mods(&mut h, Modifiers::NONE);
|
||||
let r = selection(&h);
|
||||
assert!(r.x0.abs_diff(170) <= 1 && r.x1.abs_diff(230) <= 1 && r.y0.abs_diff(120) <= 1 && r.y1.abs_diff(180) <= 1, "{r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifiers_held_before_the_drag_pick_the_mode_not_the_shape() {
|
||||
// #188: ⇧ held at the press adds (no square); released and pressed again it constrains.
|
||||
let mut h = harness(Tool::RectMarquee);
|
||||
h.state_mut().run("select.rect", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
mods(&mut h, Modifiers::SHIFT);
|
||||
press_at(&mut h, 100.0, 100.0, Modifiers::SHIFT);
|
||||
move_to(&mut h, 180.0, 130.0);
|
||||
assert!(readout(&h, 80, 30), "not squared by the add-mode ⇧");
|
||||
release_at(&mut h, 180.0, 130.0, Modifiers::SHIFT);
|
||||
mods(&mut h, Modifiers::NONE);
|
||||
assert_eq!(selection(&h), Rect::new(10, 10, 180, 130), "added to the first selection");
|
||||
let st = h.state().session.active().unwrap();
|
||||
let sel = st.doc.selection.as_ref().unwrap();
|
||||
assert!(sel.sample_channel(20, 20, 0) > 0.99 && sel.sample_channel(150, 120, 0) > 0.99 && sel.sample_channel(60, 60, 0) < 0.01);
|
||||
// ⇧ at the press, released, pressed again: add mode and a square.
|
||||
let mut h = harness(Tool::RectMarquee);
|
||||
h.state_mut().run("select.rect", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
mods(&mut h, Modifiers::SHIFT);
|
||||
press_at(&mut h, 100.0, 100.0, Modifiers::SHIFT);
|
||||
mods(&mut h, Modifiers::NONE);
|
||||
move_to(&mut h, 180.0, 130.0);
|
||||
mods(&mut h, Modifiers::SHIFT);
|
||||
move_to(&mut h, 180.0, 131.0);
|
||||
assert!(readout(&h, 80, 80));
|
||||
release_at(&mut h, 180.0, 131.0, Modifiers::SHIFT);
|
||||
mods(&mut h, Modifiers::NONE);
|
||||
let st = h.state().session.active().unwrap();
|
||||
let sel = st.doc.selection.as_ref().unwrap();
|
||||
assert!(sel.sample_channel(20, 20, 0) > 0.99, "still added");
|
||||
assert!(sel.sample_channel(170, 170, 0) > 0.99 && sel.sample_channel(170, 185, 0) < 0.01, "an 80 px square");
|
||||
// ⌥ held at the press subtracts.
|
||||
let mut h = harness(Tool::RectMarquee);
|
||||
h.state_mut().run("select.rect", json!({"x": 0, "y": 0, "width": 400, "height": 300})).unwrap();
|
||||
mods(&mut h, Modifiers::ALT);
|
||||
press_at(&mut h, 100.0, 100.0, Modifiers::ALT);
|
||||
release_at(&mut h, 150.0, 120.0, Modifiers::ALT);
|
||||
mods(&mut h, Modifiers::NONE);
|
||||
let st = h.state().session.active().unwrap();
|
||||
let sel = st.doc.selection.as_ref().unwrap();
|
||||
assert!(sel.sample_channel(120, 110, 0) < 0.01 && sel.sample_channel(90, 110, 0) > 0.99, "subtracted, not centred");
|
||||
}
|
||||
|
||||
/// `cargo test --release -p photocraft-ui-egui marquee_drag_bench -- --ignored --nocapture`
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn marquee_drag_bench() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 6000, "height": 4000})).unwrap();
|
||||
app.run("filter.render.clouds", json!({})).unwrap();
|
||||
app.sync_views();
|
||||
app.ui.tool = Tool::RectMarquee;
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(
|
||||
|ui, app: &mut PhotocraftApp| {
|
||||
if !ui.ctx().fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
|
||||
return;
|
||||
}
|
||||
egui::CentralPanel::default().show(ui, |ui| crate::canvas::document_area(app, ui));
|
||||
},
|
||||
app,
|
||||
);
|
||||
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
|
||||
h.run_steps(6);
|
||||
press_at(&mut h, 1000.0, 1000.0, Modifiers::NONE);
|
||||
mods(&mut h, Modifiers::SHIFT | Modifiers::ALT);
|
||||
let n = 60;
|
||||
let t0 = std::time::Instant::now();
|
||||
for i in 0..n {
|
||||
let p = screen(&h, 1500.0 + i as f32 * 20.0, 1300.0 + i as f32 * 7.0);
|
||||
h.event(egui::Event::PointerMoved(p));
|
||||
h.step();
|
||||
}
|
||||
let ms = t0.elapsed().as_secs_f64() * 1e3 / f64::from(n);
|
||||
eprintln!("marquee drag on 6000x4000 (shift+alt, readout): {ms:.2} ms per frame");
|
||||
assert!(h.query_by_label("W:").is_some());
|
||||
}
|
||||
Reference in new issue
Block a user