Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e47d542680 | ||
|
|
d8759492df | ||
|
|
6ba5e0585e | ||
|
|
4c1e16d791 | ||
|
|
dd8f144770 | ||
|
|
6c131431ed | ||
|
|
8ae59bf6b7 | ||
|
|
b47f611e18 | ||
|
|
02d02f660f |
No files matched your search
@@ -274,9 +274,12 @@ fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
fileio::write_atomic(std::path::Path::new(path), bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Linux: show the dialog for `request` over `parent` on a thread of its own, answering on the
|
||||
/// receiver. Shown on the UI thread, nothing would answer the compositor meanwhile, which then
|
||||
/// offers to kill the window as not responding (#592).
|
||||
/// Linux and macOS: show the dialog for `request` over `parent` on a thread of its own, answering
|
||||
/// on the receiver. Shown on the UI thread, nothing would answer the compositor meanwhile on Linux,
|
||||
/// which then offers to kill the window as not responding (#592); on macOS the panel ran modally
|
||||
/// inside the window's event handler, and resizing it delivered window events to that handler
|
||||
/// again, which crashed the app (#867). From another thread, rfd runs the panel on the main thread
|
||||
/// from the run loop, outside the handler.
|
||||
fn start_pick(request: PickRequest, parent: &Parent) -> Option<std::sync::mpsc::Receiver<Vec<String>>> {
|
||||
let dialog = file_dialog(&request, parent);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
@@ -296,9 +299,9 @@ fn services(parent: Parent) -> Services {
|
||||
pick_open: Some(Box::new(move |pick: &FilePick| pick_now(PickRequest::Open(pick.clone()), &p1).into_iter().next())),
|
||||
pick_open_multi: Some(Box::new(move || pick_now(PickRequest::OpenMany, &p2))),
|
||||
pick_save: Some(Box::new(move |pick: &FilePick| pick_now(PickRequest::Save(pick.clone()), &p3).into_iter().next())),
|
||||
// Windows and macOS dialogs run the window's events while they are open; Linux's don't.
|
||||
start_pick: cfg!(all(unix, not(target_os = "macos")))
|
||||
.then(|| Box::new(move |request: PickRequest| start_pick(request, &p5)) as vectorcraft_ui_egui::picks::StartPick),
|
||||
// Windows dialogs run the window's events while they are open; Linux's don't, and macOS
|
||||
// ones re-enter the window's event handler (#867).
|
||||
start_pick: cfg!(unix).then(|| Box::new(move |request: PickRequest| start_pick(request, &p5)) as vectorcraft_ui_egui::picks::StartPick),
|
||||
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
|
||||
write: Some(Box::new(write_file)),
|
||||
// Background Save and Export write from a worker thread.
|
||||
|
||||
+10
-2
@@ -960,8 +960,12 @@ impl Document {
|
||||
if c.is_layer() {
|
||||
collect(c, rect, all, out);
|
||||
} else if let Some(b) = c.geometric_bounds()
|
||||
&& rect.contains(Point::new(b.x0, b.y0))
|
||||
&& rect.contains(Point::new(b.x1, b.y1))
|
||||
// Inside it, edges included: art flush with the right or bottom edge is on it too
|
||||
// (`Rect::contains` leaves those out, #890).
|
||||
&& b.x0 >= rect.x0
|
||||
&& b.y0 >= rect.y0
|
||||
&& b.x1 <= rect.x1
|
||||
&& b.y1 <= rect.y1
|
||||
{
|
||||
out.push(c.id);
|
||||
}
|
||||
@@ -1174,6 +1178,10 @@ mod tests {
|
||||
let wide = Rect::new(-1.0, -1.0, 40.0, 15.0);
|
||||
assert_eq!(d.art_on_artboard(Rect::new(-1.0, -1.0, 15.0, 15.0), false), vec![a]);
|
||||
assert_eq!(d.art_on_artboard(Rect::new(-1.0, -1.0, 25.0, 15.0), false), vec![a], "b only half inside");
|
||||
// Flush with the artboard's edges counts as inside: the right and bottom ones too (#890).
|
||||
for board in [Rect::new(0.0, 0.0, 10.0, 10.0), Rect::new(-5.0, 0.0, 10.0, 15.0), Rect::new(0.0, -5.0, 15.0, 10.0)] {
|
||||
assert_eq!(d.art_on_artboard(board, false), vec![a], "{board:?}");
|
||||
}
|
||||
d.node_mut(a).unwrap().locked = true;
|
||||
assert_eq!(d.art_on_artboard(wide, false), vec![b]);
|
||||
// Move Locked and Hidden Artwork with Artboard (#394): hidden art stays too, unless on.
|
||||
|
||||
+16
-10
@@ -32,7 +32,7 @@ use vectorcraft_geom::{Anchor, PathData, Point, Rect};
|
||||
|
||||
use crate::bbox::move_delta;
|
||||
use crate::corners::{self, CornerDrag, over_widget};
|
||||
use crate::guides::{HandleSnap, PointSnap, Targets};
|
||||
use crate::guides::{HandleSnap, Leave, PointSnap, Targets};
|
||||
use crate::meshedit::MeshEdit;
|
||||
use crate::pathtype::{self, BracketDrag, over_bracket};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
@@ -428,12 +428,12 @@ impl Tool for DirectSelectionTool {
|
||||
self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection)));
|
||||
}
|
||||
let mut d = move_delta(start, p, ev.mods.shift);
|
||||
// The grabbed anchor snaps and the others follow it; Shift keeps the angle instead.
|
||||
// The grabbed anchor snaps and the others follow it. Shift keeps the move at its
|
||||
// angle, and the anchor slides along it onto what Smart Guides find there (#886).
|
||||
self.guides.clear();
|
||||
if !ev.mods.shift
|
||||
&& let Some(snap) = &self.anchor_snap
|
||||
{
|
||||
let (q, guides) = snap.snap(cx, grab + d);
|
||||
if let Some(snap) = &self.anchor_snap {
|
||||
let shift = ev.mods.shift.then(|| Leave::segment(cx, grab, true));
|
||||
let (q, guides) = snap.snap_from(cx, grab + (p - start), shift.as_ref());
|
||||
(d, self.guides) = (q - grab, guides);
|
||||
}
|
||||
out.push(Action::Preview("path.moveAnchors".into(), json!({"dx": d.x, "dy": d.y})));
|
||||
@@ -1022,10 +1022,16 @@ mod tests {
|
||||
// In line with B's own bottom-left corner, which stays put.
|
||||
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (303.0, 330.0), none);
|
||||
assert_eq!((v, labels), (json!({"dx": 0.0, "dy": 30.0}), vec!["align".to_string()]));
|
||||
// Shift keeps the move at 45° steps, unsnapped.
|
||||
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (203.0, 199.0), Mods { shift: true, ..none });
|
||||
let s = move_delta(Point::new(300.0, 300.0), Point::new(203.0, 199.0), true);
|
||||
assert_eq!((v, labels), (json!({"dx": s.x, "dy": s.y}), vec![]));
|
||||
// Shift keeps the move at 45° steps and still snaps along that line (#886): on the
|
||||
// diagonal from B's corner, onto A's bottom-right corner…
|
||||
let shift = Mods { shift: true, ..none };
|
||||
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (203.0, 199.0), shift);
|
||||
let near = |v: &serde_json::Value, (x, y): (f64, f64)| (v["dx"].as_f64().unwrap() - x).hypot(v["dy"].as_f64().unwrap() - y) < 1e-9;
|
||||
assert!(near(&v, (-100.0, -100.0)) && labels == ["anchor"], "{v} {labels:?}");
|
||||
// …and with nothing in reach, at the pointer's distance along its angle.
|
||||
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (380.0, 302.0), shift);
|
||||
let s = move_delta(Point::new(300.0, 300.0), Point::new(380.0, 302.0), true);
|
||||
assert!(near(&v, (s.x, s.y)) && labels.is_empty(), "{v} {labels:?}");
|
||||
// Two anchors: the one pressed on (B's top-right) lands on A's corner.
|
||||
let two = anchors_of(b, &[(0, 0), (0, 1)]);
|
||||
let c = cx(&d, &two, &p);
|
||||
|
||||
@@ -512,13 +512,16 @@ fn cursor_icon(c: Cursor) -> egui::CursorIcon {
|
||||
fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: egui::Rect) {
|
||||
let line = ui.ctx().options(|o| o.input_options.line_scroll_speed);
|
||||
let wheel_zooms = app.session.prefs.zoom_with_mouse_wheel;
|
||||
let alt_id = egui::Id::new("canvas-alt-wheel");
|
||||
let alt_before = ui.data(|d| d.get_temp(alt_id)).unwrap_or(false);
|
||||
let mut alt_turn = alt_before;
|
||||
let turn_id = egui::Id::new("canvas-wheel-turn");
|
||||
let turn_before: WheelTurn = ui.data(|d| d.get_temp(turn_id)).unwrap_or_default();
|
||||
let mut turn = turn_before;
|
||||
let (pointer, m, space, (factor, scroll)) =
|
||||
ui.input(|i| (i.pointer.clone(), i.modifiers, i.key_down(egui::Key::Space), wheel(i, wheel_zooms, line, rect.height(), &mut alt_turn)));
|
||||
if alt_turn != alt_before {
|
||||
ui.data_mut(|d| d.insert_temp(alt_id, alt_turn));
|
||||
ui.input(|i| (i.pointer.clone(), i.modifiers, i.key_down(egui::Key::Space), wheel(i, wheel_zooms, line, rect.height(), &mut turn)));
|
||||
if turn != turn_before {
|
||||
ui.data_mut(|d| d.insert_temp(turn_id, turn));
|
||||
}
|
||||
if turn.glide != 0.0 {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
let v = *app.view().unwrap_or(&View::default());
|
||||
let xf = Xf::new(rect, &v);
|
||||
@@ -717,6 +720,18 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
|
||||
/// How much one point of wheel motion zooms (as `exp(points × WHEEL_ZOOM)`).
|
||||
const WHEEL_ZOOM: f64 = 0.01;
|
||||
/// How fast a wheel notch's zoom glides in with Zoom with Mouse Wheel: the time constant (s) of
|
||||
/// the part still to come.
|
||||
const WHEEL_GLIDE: f64 = 0.05;
|
||||
|
||||
/// What the wheel did across frames ([`wheel`]).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
struct WheelTurn {
|
||||
/// The last wheel turn was an Alt-wheel one.
|
||||
alt: bool,
|
||||
/// The zoom (natural log) wheel notches still have to glide in.
|
||||
glide: f64,
|
||||
}
|
||||
/// Animated Zoom: how much one point of sideways drag zooms (100 points to the right double it).
|
||||
const SCRUB_ZOOM: f64 = std::f64::consts::LN_2 / 100.0;
|
||||
/// Animated Zoom: how far the pointer moves before a press is a drag, in points.
|
||||
@@ -754,19 +769,19 @@ fn marquee(p: &egui::Painter, r: egui::Rect) {
|
||||
/// a zoom factor (about the pointer) and a scroll (screen points the content moves). The wheel scrolls, Cmd- and Alt-wheel (Option
|
||||
/// on the Mac) zoom. With General › Zoom with Mouse Wheel (`wheel_zooms`) the wheel and Alt-wheel
|
||||
/// zoom, Shift-wheel scrolls up and down and Cmd/Ctrl-wheel sideways. `line` and `page`: points
|
||||
/// per wheel line and page. `alt_turn`: the last wheel turn was an Alt-wheel one (kept by the
|
||||
/// caller across frames).
|
||||
fn wheel(i: &egui::InputState, wheel_zooms: bool, line: f32, page: f32, alt_turn: &mut bool) -> (f64, egui::Vec2) {
|
||||
/// per wheel line and page. `turn`: kept by the caller across frames. A mouse wheel's notches zoom
|
||||
/// in a glide over a few frames, as scrolling does (#888); a trackpad's fine steps at once.
|
||||
fn wheel(i: &egui::InputState, wheel_zooms: bool, line: f32, page: f32, turn: &mut WheelTurn) -> (f64, egui::Vec2) {
|
||||
// Fingers on a touch screen pan the content with them (a trackpad sends scrolls instead).
|
||||
let pan = i.multi_touch().map_or(egui::Vec2::ZERO, |t| t.translation_delta);
|
||||
if !wheel_zooms {
|
||||
// egui makes Cmd-wheel (and a pinch) its zoom and the rest a scroll it spreads over a few
|
||||
// frames: the rest of an Alt-wheel turn zooms too, however soon Alt is let go.
|
||||
if let Some(m) = i.events.iter().rev().find_map(|e| if let egui::Event::MouseWheel { modifiers, .. } = e { Some(modifiers) } else { None }) {
|
||||
*alt_turn = m.alt && !m.command;
|
||||
turn.alt = m.alt && !m.command;
|
||||
}
|
||||
let (zoom, scroll) = (f64::from(i.zoom_delta()), i.smooth_scroll_delta);
|
||||
return if *alt_turn && scroll != egui::Vec2::ZERO {
|
||||
return if turn.alt && scroll != egui::Vec2::ZERO {
|
||||
(zoom * (f64::from(scroll.x + scroll.y) * WHEEL_ZOOM).exp(), pan)
|
||||
} else {
|
||||
(zoom, scroll + pan)
|
||||
@@ -787,16 +802,23 @@ fn wheel(i: &egui::InputState, wheel_zooms: bool, line: f32, page: f32, alt_turn
|
||||
scroll.x += d.x + d.y;
|
||||
} else if modifiers.shift {
|
||||
scroll.y += d.x + d.y;
|
||||
} else {
|
||||
} else if *unit == egui::MouseWheelUnit::Point {
|
||||
zoom *= (f64::from(d.y) * WHEEL_ZOOM).exp();
|
||||
scroll.x += d.x;
|
||||
} else {
|
||||
turn.glide += f64::from(d.y) * WHEEL_ZOOM;
|
||||
scroll.x += d.x;
|
||||
}
|
||||
}
|
||||
egui::Event::Zoom(f) => zoom *= f64::from(*f),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(zoom, scroll)
|
||||
// This frame's part of the glide: what is left shrinks by e every WHEEL_GLIDE seconds.
|
||||
let part = if turn.glide.abs() < 1e-3 { 1.0 } else { 1.0 - (-f64::from(i.stable_dt.min(0.1)) / WHEEL_GLIDE).exp() };
|
||||
let step = turn.glide * part;
|
||||
turn.glide -= step;
|
||||
(zoom * step.exp(), scroll)
|
||||
}
|
||||
|
||||
/// Enable Touch Gestures: this frame's finger contacts on the canvas ([`crate::touch`]). A second
|
||||
@@ -2868,6 +2890,43 @@ mod tests {
|
||||
assert!((zoom - 1.0).abs() < 1e-9 && scroll.x < -1.0 && scroll.y == 0.0, "on: Cmd-wheel scrolls sideways ({zoom}, {scroll:?})");
|
||||
}
|
||||
|
||||
/// #888: with Zoom with Mouse Wheel, a wheel notch's zoom glides in over a few frames, as a
|
||||
/// scroll does, instead of jumping on the frame it arrives; it ends at the same zoom.
|
||||
#[test]
|
||||
fn wheel_notches_zoom_in_a_glide() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.session.execute("prefs.set", &json!({"key": "zoomWithMouseWheel", "value": true})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let run = |app: &mut VectorcraftApp| {
|
||||
let mut raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() };
|
||||
app.raw_input_hook(&mut raw);
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
run(&mut app);
|
||||
let at = app.canvas_rect.unwrap().center();
|
||||
let before = app.view().unwrap().zoom;
|
||||
crate::tests_synthetic::control(&mut app, &ctx, "ui.wheel", json!({"x": at.x, "y": at.y, "dy": 1}));
|
||||
// The first frame the zoom moves in (the event may land a frame later).
|
||||
let mut first = 1.0;
|
||||
for _ in 0..3 {
|
||||
run(&mut app);
|
||||
first = app.view().unwrap().zoom / before;
|
||||
if first != 1.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for _ in 0..60 {
|
||||
run(&mut app);
|
||||
}
|
||||
let done = app.view().unwrap().zoom / before;
|
||||
assert!(done > 1.01, "the notch zooms in ({done})");
|
||||
assert!(first > 1.0 && first < done, "the first frame takes only part of it ({first} of {done})");
|
||||
run(&mut app);
|
||||
assert_eq!(app.view().unwrap().zoom / before, done, "and it stops");
|
||||
}
|
||||
|
||||
/// Two fingers dragged together on a touch screen pan the canvas with them, under either
|
||||
/// Zoom with Mouse Wheel setting, and draw nothing (#449).
|
||||
#[test]
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
//!
|
||||
//! The app asks for a path where it needs one (Open, Save As, Export, Place, Relink, presets…)
|
||||
//! and goes on with it at once, so a dialog shown on the UI thread holds the window until it
|
||||
//! closes. On Windows and macOS the dialog runs the window's events meanwhile; on Linux nothing
|
||||
//! does, so a Wayland compositor finds the window not answering and offers to kill it. There the
|
||||
//! host shows dialogs on another thread ([`Services::start_pick`]): asking returns no path for now,
|
||||
//! closes. On Windows the dialog runs the window's events meanwhile; on Linux nothing does, so a
|
||||
//! Wayland compositor finds the window not answering and offers to kill it, and on macOS they
|
||||
//! re-enter the window's event handler, which crashed the app when the panel was resized (#867).
|
||||
//! There the host shows dialogs on another thread ([`Services::start_pick`]): asking returns no path for now,
|
||||
//! and when the dialog answers, what asked (the command, the dialog's OK, a panel's button: the
|
||||
//! *entry*) runs again and gets the picked path at once.
|
||||
|
||||
|
||||
Reference in new issue
Block a user