mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 19:08:05 +08:00
Merge origin/master into the async record refactor
Scaffolding merge for the reconcile; the final series to master is authored fresh. Rank plumbing resolves to our axis-IR model, the node macro and the LaneSource render walk stay ours, master's vector restructure and gradient vocabulary are adopted, and the paint and appearance adoption is deliberately deferred behind our fill and stroke markers.
This commit is contained in:
@@ -9,19 +9,21 @@ use std::sync::mpsc::{Receiver, SyncSender};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEvent};
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::{ElementState, MouseButton, StartCause, WindowEvent};
|
||||
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use crate::dirs;
|
||||
use crate::event::{AppEvent, AppEventScheduler};
|
||||
use crate::input::InputState;
|
||||
use crate::persist;
|
||||
use crate::preferences;
|
||||
use crate::render::{RenderError, RenderState};
|
||||
use crate::ui::{UiCommand, UiInstance};
|
||||
use crate::ui::{InputEvent, UiCommand, UiInstance};
|
||||
use crate::window::Window;
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Preferences};
|
||||
use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
pub(crate) struct App {
|
||||
@@ -33,8 +35,7 @@ pub(crate) struct App {
|
||||
window_maximized: bool,
|
||||
window_fullscreen: bool,
|
||||
window_pending_drag: bool,
|
||||
pointer_position: PhysicalPosition<f64>,
|
||||
pointer_lock_position: Option<PhysicalPosition<f64>>,
|
||||
input_state: InputState,
|
||||
ui_scale: f64,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
@@ -111,8 +112,7 @@ impl App {
|
||||
window_maximized: false,
|
||||
window_fullscreen: false,
|
||||
window_pending_drag: false,
|
||||
pointer_position: Default::default(),
|
||||
pointer_lock_position: Default::default(),
|
||||
input_state: InputState::new(),
|
||||
ui_scale: 1.,
|
||||
app_event_receiver,
|
||||
app_event_scheduler,
|
||||
@@ -130,8 +130,8 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run(mut self, event_loop: EventLoop) -> ExitReason {
|
||||
event_loop.run_app(&mut self).unwrap();
|
||||
pub(crate) fn run(mut self, mut event_loop: EventLoop) -> ExitReason {
|
||||
event_loop.run_app_on_demand(&mut self).unwrap();
|
||||
self.exit_reason
|
||||
}
|
||||
|
||||
@@ -269,6 +269,8 @@ impl App {
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
|
||||
self.input_state.set_viewport_info(x, y, width, height, self.window_scale);
|
||||
|
||||
if let Some(render_state) = &mut self.render_state
|
||||
&& let Some(window) = &self.window
|
||||
{
|
||||
@@ -283,6 +285,9 @@ impl App {
|
||||
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowUpdateDirectInput { enabled } => {
|
||||
self.input_state.set_direct_input(enabled);
|
||||
}
|
||||
DesktopFrontendMessage::UpdateUIScale { scale } => {
|
||||
self.ui_scale = scale;
|
||||
self.resize();
|
||||
@@ -347,7 +352,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PointerLock => {
|
||||
self.pointer_lock_position = Some(self.pointer_position);
|
||||
self.input_state.lock_pointer();
|
||||
if let Some(window) = &self.window {
|
||||
window.start_pointer_lock();
|
||||
}
|
||||
@@ -540,26 +545,26 @@ impl ApplicationHandler for App {
|
||||
|
||||
fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||
// Handle pointer lock release
|
||||
if let Some(pointer_lock_position) = self.pointer_lock_position
|
||||
&& let WindowEvent::PointerButton {
|
||||
state: ElementState::Released,
|
||||
button: ButtonSource::Mouse(MouseButton::Left),
|
||||
..
|
||||
} = event
|
||||
if let WindowEvent::PointerButton {
|
||||
state: ElementState::Released,
|
||||
button,
|
||||
..
|
||||
} = &event && button.clone().mouse_button() == Some(MouseButton::Left)
|
||||
&& let Some(pointer_lock_position) = self.input_state.unlock_pointer()
|
||||
{
|
||||
self.pointer_lock_position = None;
|
||||
if let Some(window) = &self.window {
|
||||
window.end_pointer_lock();
|
||||
}
|
||||
self.ui.send(UiCommand::Input(WindowEvent::PointerMoved {
|
||||
device_id: None,
|
||||
position: pointer_lock_position,
|
||||
primary: true,
|
||||
source: winit::event::PointerSource::Mouse,
|
||||
}));
|
||||
self.ui.send(UiCommand::Input(
|
||||
InputEvent::pointer().position(pointer_lock_position).moved().modifiers(self.input_state.modifiers()).build(),
|
||||
));
|
||||
}
|
||||
|
||||
self.ui.send(UiCommand::Input(event.clone()));
|
||||
self.input_state.process(
|
||||
&event,
|
||||
|message| self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Input(message))),
|
||||
|input| self.ui.send(UiCommand::Input(input)),
|
||||
);
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
@@ -616,50 +621,20 @@ impl ApplicationHandler for App {
|
||||
}
|
||||
}
|
||||
|
||||
// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
|
||||
WindowEvent::PointerButton {
|
||||
button: ButtonSource::Mouse(button),
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
} => {
|
||||
let mouse_keys = match button {
|
||||
MouseButton::Back => Some(MouseKeys::BACK),
|
||||
MouseButton::Forward => Some(MouseKeys::FORWARD),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(mouse_keys) = mouse_keys {
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
|
||||
editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
|
||||
editor_mouse_state: Default::default(),
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerLeft { position: Some(position), .. } | WindowEvent::PointerEntered { position, .. }
|
||||
if self.pointer_lock_position.is_none() =>
|
||||
WindowEvent::PointerMoved { .. } | WindowEvent::PointerLeft { position: Some(_), .. } | WindowEvent::PointerEntered { .. }
|
||||
if !self.input_state.pointer_locked() && self.window_pending_drag =>
|
||||
{
|
||||
self.pointer_position = position;
|
||||
|
||||
if self.window_pending_drag {
|
||||
self.window_pending_drag = false;
|
||||
if let Some(window) = &self.window {
|
||||
window.start_drag();
|
||||
}
|
||||
self.window_pending_drag = false;
|
||||
if let Some(window) = &self.window {
|
||||
window.start_drag();
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PointerButton {
|
||||
button: ButtonSource::Mouse(MouseButton::Left),
|
||||
button,
|
||||
state: ElementState::Released,
|
||||
..
|
||||
} => {
|
||||
} if button.clone().mouse_button() == Some(MouseButton::Left) => {
|
||||
self.window_pending_drag = false;
|
||||
}
|
||||
|
||||
@@ -668,7 +643,7 @@ impl ApplicationHandler for App {
|
||||
}
|
||||
|
||||
fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option<winit::event::DeviceId>, event: winit::event::DeviceEvent) {
|
||||
if self.pointer_lock_position.is_some()
|
||||
if self.input_state.pointer_locked()
|
||||
&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
|
||||
{
|
||||
let message = DesktopWrapperMessage::PointerLockMove { x, y };
|
||||
|
||||
453
desktop/src/input.rs
Normal file
453
desktop/src/input.rs
Normal file
@@ -0,0 +1,453 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::dpi::PhysicalPosition;
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerSource, TabletToolData, TabletToolKind, WindowEvent};
|
||||
use winit::keyboard::ModifiersState;
|
||||
|
||||
use crate::ui::{InputEvent, MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT, PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
||||
use crate::wrapper::messages::{EditorPointerState, InputMessage, ModifierKeys, MouseKeys, ScrollDelta};
|
||||
|
||||
// Marks input event as observe-only, meaning it should not result in any input messages being send by the frontend.
|
||||
// And should instead be ignored by frontend input processing and only be used to update the UI state (hover, cursor).
|
||||
// TODO(Timon): Remove and find a less hacky solution
|
||||
trait ObserveOnlyExt {
|
||||
fn observe_only(self) -> Self;
|
||||
}
|
||||
impl ObserveOnlyExt for crate::ui::InputEventBuilder {
|
||||
fn observe_only(self) -> Self {
|
||||
self.num_lock(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InputState {
|
||||
start: Instant,
|
||||
viewport_info: Option<ViewportInfo>,
|
||||
direct_input: bool,
|
||||
modifiers: ModifiersState,
|
||||
pointer_position: PhysicalPosition<f64>,
|
||||
pointer_state: PointerState,
|
||||
click_tracker: ClickTracker,
|
||||
shake_tracker: ShakeTracker,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
start: Instant::now(),
|
||||
viewport_info: None,
|
||||
direct_input: false,
|
||||
modifiers: ModifiersState::default(),
|
||||
pointer_position: PhysicalPosition::default(),
|
||||
pointer_state: PointerState::Hover { route: Route::Ui },
|
||||
click_tracker: ClickTracker::default(),
|
||||
shake_tracker: ShakeTracker::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_info(&mut self, x: f64, y: f64, width: f64, height: f64, scale: f64) {
|
||||
self.viewport_info = Some(ViewportInfo { x, y, width, height, scale });
|
||||
}
|
||||
|
||||
pub(crate) fn set_direct_input(&mut self, enabled: bool) {
|
||||
self.direct_input = enabled;
|
||||
}
|
||||
|
||||
pub(crate) fn lock_pointer(&mut self) {
|
||||
self.pointer_state = match self.pointer_state {
|
||||
PointerState::Hover { route } => PointerState::Locked {
|
||||
route,
|
||||
keys: MouseKeys::empty(),
|
||||
position: self.pointer_position,
|
||||
},
|
||||
PointerState::Stroke { route, keys } | PointerState::Locked { route, keys, .. } => PointerState::Locked {
|
||||
route,
|
||||
keys,
|
||||
position: self.pointer_position,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn unlock_pointer(&mut self) -> Option<PhysicalPosition<f64>> {
|
||||
let PointerState::Locked {
|
||||
route: resume,
|
||||
keys,
|
||||
position: restore,
|
||||
} = self.pointer_state
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
self.pointer_position = restore;
|
||||
self.pointer_state = match keys.is_empty() {
|
||||
true => PointerState::Hover { route: Route::Ui },
|
||||
false => PointerState::Stroke { route: resume, keys },
|
||||
};
|
||||
Some(restore)
|
||||
}
|
||||
|
||||
pub(crate) fn pointer_locked(&self) -> bool {
|
||||
matches!(self.pointer_state, PointerState::Locked { .. })
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers(&self) -> ModifiersState {
|
||||
self.modifiers
|
||||
}
|
||||
|
||||
pub(crate) fn process(&mut self, event: &WindowEvent, mut editor_callback: impl FnMut(InputMessage), mut ui_callback: impl FnMut(InputEvent)) {
|
||||
match event {
|
||||
WindowEvent::PointerMoved { position, source, .. } => {
|
||||
self.pointer_position = *position;
|
||||
|
||||
let route = match self.pointer_state {
|
||||
PointerState::Hover { .. } => {
|
||||
let next = self.route(*position);
|
||||
self.pointer_state = PointerState::Hover { route: next };
|
||||
next
|
||||
}
|
||||
PointerState::Stroke { route, .. } => route,
|
||||
PointerState::Locked { keys, route: resume, .. } => match keys.is_empty() {
|
||||
true => Route::Ui,
|
||||
false => resume,
|
||||
},
|
||||
};
|
||||
match route {
|
||||
Route::Ui => ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).build()),
|
||||
Route::Editor => {
|
||||
if !self.pointer_locked() {
|
||||
ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).observe_only().build());
|
||||
}
|
||||
let editor_mouse_state = match source {
|
||||
PointerSource::TabletTool { kind, data } => self.tablet_pointer_state(kind, data),
|
||||
_ => self.pointer_state(),
|
||||
};
|
||||
let modifier_keys = self.modifier_keys();
|
||||
if self.shake_tracker.detect(*position, self.start.elapsed()) {
|
||||
editor_callback(InputMessage::PointerShake { editor_mouse_state, modifier_keys });
|
||||
}
|
||||
editor_callback(InputMessage::PointerMove { editor_mouse_state, modifier_keys });
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::PointerEntered { position, .. } => {
|
||||
self.pointer_position = *position;
|
||||
ui_callback(InputEvent::pointer().position(*position).entered().modifiers(self.modifiers).build())
|
||||
}
|
||||
WindowEvent::PointerLeft { position: Some(position), .. } => {
|
||||
self.pointer_position = *position;
|
||||
ui_callback(InputEvent::pointer().position(*position).exited().modifiers(self.modifiers).build())
|
||||
}
|
||||
WindowEvent::PointerLeft { position: None, .. } => ui_callback(InputEvent::pointer().exited().modifiers(self.modifiers).build()),
|
||||
WindowEvent::PointerButton { state, button, position, .. } => {
|
||||
self.pointer_position = *position;
|
||||
|
||||
let mouse_button = button.clone().mouse_button();
|
||||
let keys = match mouse_button {
|
||||
Some(MouseButton::Left) => MouseKeys::LEFT,
|
||||
Some(MouseButton::Right) => MouseKeys::RIGHT,
|
||||
Some(MouseButton::Middle) => MouseKeys::MIDDLE,
|
||||
Some(MouseButton::Back) => MouseKeys::BACK,
|
||||
Some(MouseButton::Forward) => MouseKeys::FORWARD,
|
||||
_ => MouseKeys::NONE,
|
||||
};
|
||||
|
||||
let (pointer, route) = match self.pointer_state {
|
||||
PointerState::Hover { route } => match (state.is_pressed(), keys.is_empty()) {
|
||||
(true, false) => {
|
||||
let route = self.route(*position);
|
||||
(PointerState::Stroke { route, keys }, route)
|
||||
}
|
||||
(true, true) => (PointerState::Hover { route }, self.route(*position)),
|
||||
(false, _) => (PointerState::Hover { route }, route),
|
||||
},
|
||||
PointerState::Stroke { route, keys: mut held } => {
|
||||
match state.is_pressed() {
|
||||
true => held.insert(keys),
|
||||
false => held.remove(keys),
|
||||
}
|
||||
match held.is_empty() {
|
||||
true => (PointerState::Hover { route }, route),
|
||||
false => (PointerState::Stroke { route, keys: held }, route),
|
||||
}
|
||||
}
|
||||
PointerState::Locked { route, keys: mut held, position } => {
|
||||
let resume = if state.is_pressed() && held.is_empty() { Route::Ui } else { route };
|
||||
match state.is_pressed() {
|
||||
true => held.insert(keys),
|
||||
false => held.remove(keys),
|
||||
}
|
||||
(PointerState::Locked { route: resume, keys: held, position }, Route::Ui)
|
||||
}
|
||||
};
|
||||
self.pointer_state = pointer;
|
||||
|
||||
let count = mouse_button.map_or(1, |button| self.click_tracker.input(*position, button, *state));
|
||||
|
||||
let back_or_forward = matches!(mouse_button, Some(MouseButton::Back | MouseButton::Forward));
|
||||
let pointer = InputEvent::pointer().position(*position);
|
||||
let input = match state {
|
||||
ElementState::Pressed => pointer.pressed(button.clone(), count),
|
||||
ElementState::Released => pointer.released(button.clone(), count),
|
||||
}
|
||||
.modifiers(self.modifiers);
|
||||
if self.pointer_locked() || keys.is_empty() || !(back_or_forward || route == Route::Editor) {
|
||||
ui_callback(input.build());
|
||||
return;
|
||||
}
|
||||
if route == Route::Editor {
|
||||
ui_callback(input.observe_only().build());
|
||||
}
|
||||
|
||||
let editor_mouse_state = match button {
|
||||
ButtonSource::TabletTool { kind, data, .. } => self.tablet_pointer_state(kind, data),
|
||||
_ => self.pointer_state(),
|
||||
};
|
||||
let modifier_keys = self.modifier_keys();
|
||||
match state {
|
||||
ElementState::Pressed => editor_callback(InputMessage::PointerDown { editor_mouse_state, modifier_keys }),
|
||||
ElementState::Released if count % 2 == 0 => {
|
||||
editor_callback(InputMessage::PointerUp { editor_mouse_state, modifier_keys });
|
||||
editor_callback(InputMessage::DoubleClick {
|
||||
editor_mouse_state: EditorPointerState {
|
||||
mouse_keys: keys,
|
||||
..editor_mouse_state
|
||||
},
|
||||
modifier_keys,
|
||||
});
|
||||
}
|
||||
ElementState::Released => editor_callback(InputMessage::PointerUp { editor_mouse_state, modifier_keys }),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
if self.pointer_locked() || self.ui_captures(self.pointer_position) {
|
||||
let input = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => InputEvent::pointer().scrolled_lines(f64::from(*x), f64::from(*y)),
|
||||
MouseScrollDelta::PixelDelta(position) => InputEvent::pointer().scrolled_pixels(position.x, position.y),
|
||||
};
|
||||
ui_callback(input.modifiers(self.modifiers).build());
|
||||
return;
|
||||
}
|
||||
|
||||
let (x, y) = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => (f64::from(*x) * SCROLL_LINE_WIDTH, f64::from(*y) * SCROLL_LINE_HEIGHT),
|
||||
MouseScrollDelta::PixelDelta(position) => (position.x, position.y),
|
||||
};
|
||||
|
||||
let scroll_delta = ScrollDelta::new(-x * SCROLL_SPEED_X, -y * SCROLL_SPEED_Y, 0.);
|
||||
|
||||
editor_callback(InputMessage::WheelScroll {
|
||||
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
|
||||
modifier_keys: self.modifier_keys(),
|
||||
});
|
||||
}
|
||||
WindowEvent::PinchGesture { delta, .. } => {
|
||||
if self.pointer_locked() || self.ui_captures(self.pointer_position) || !delta.is_normal() {
|
||||
ui_callback(InputEvent::pointer().zoomed(*delta).modifiers(self.modifiers).build());
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This is a temporary solution to handle pinch gestures, we should handle pinch gestures editor-side instead.
|
||||
let scroll_delta = ScrollDelta::new(0., -delta * PINCH_ZOOM_SPEED, 0.);
|
||||
editor_callback(InputMessage::WheelScroll {
|
||||
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
|
||||
modifier_keys: self.modifier_keys() | ModifierKeys::CONTROL,
|
||||
});
|
||||
}
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
self.modifiers = modifiers.state();
|
||||
}
|
||||
WindowEvent::KeyboardInput { event, .. } => ui_callback(InputEvent::key(event).modifiers(self.modifiers).build()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale(&self) -> f64 {
|
||||
self.viewport_info.as_ref().map_or(1., |info| info.scale)
|
||||
}
|
||||
|
||||
fn ui_captures(&self, position: PhysicalPosition<f64>) -> bool {
|
||||
!self.direct_input || !self.viewport_info.as_ref().is_some_and(|info| info.contains(position))
|
||||
}
|
||||
|
||||
fn route(&self, position: PhysicalPosition<f64>) -> Route {
|
||||
if self.ui_captures(position) { Route::Ui } else { Route::Editor }
|
||||
}
|
||||
|
||||
fn pointer_keys(&self) -> MouseKeys {
|
||||
match self.pointer_state {
|
||||
PointerState::Hover { .. } => MouseKeys::empty(),
|
||||
PointerState::Stroke { keys, .. } | PointerState::Locked { keys, .. } => keys,
|
||||
}
|
||||
}
|
||||
|
||||
fn pointer_state(&self) -> EditorPointerState {
|
||||
EditorPointerState {
|
||||
editor_position: (self.pointer_position.x / self.scale(), self.pointer_position.y / self.scale()).into(),
|
||||
mouse_keys: self.pointer_keys(),
|
||||
time: Some(self.start.elapsed().as_secs_f64() * 1000.),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn tablet_pointer_state(&self, kind: &TabletToolKind, data: &TabletToolData) -> EditorPointerState {
|
||||
EditorPointerState {
|
||||
pressure: data.force.map(|force| force.normalized(None)),
|
||||
tilt: data.clone().tilt().map(|tilt| (f64::from(tilt.x), f64::from(tilt.y)).into()),
|
||||
twist: data.twist.map(f64::from),
|
||||
wheel: data.tangential_force.map(f64::from),
|
||||
eraser: matches!(kind, TabletToolKind::Eraser),
|
||||
..self.pointer_state()
|
||||
}
|
||||
}
|
||||
|
||||
fn modifier_keys(&self) -> ModifierKeys {
|
||||
let mut keys = ModifierKeys::empty();
|
||||
keys.set(ModifierKeys::SHIFT, self.modifiers.shift_key());
|
||||
keys.set(ModifierKeys::CONTROL, self.modifiers.control_key());
|
||||
keys.set(ModifierKeys::ALT, self.modifiers.alt_key());
|
||||
keys.set(ModifierKeys::META_OR_COMMAND, self.modifiers.meta_key());
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PointerState {
|
||||
Hover { route: Route },
|
||||
Stroke { route: Route, keys: MouseKeys },
|
||||
Locked { route: Route, keys: MouseKeys, position: PhysicalPosition<f64> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Route {
|
||||
Ui,
|
||||
Editor,
|
||||
}
|
||||
|
||||
struct ViewportInfo {
|
||||
x: f64,
|
||||
y: f64,
|
||||
width: f64,
|
||||
height: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl ViewportInfo {
|
||||
fn contains(&self, position: PhysicalPosition<f64>) -> bool {
|
||||
position.x >= self.x && position.y >= self.y && position.x <= self.x + self.width && position.y <= self.y + self.height
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClickTracker {
|
||||
left: ClickChains,
|
||||
right: ClickChains,
|
||||
middle: ClickChains,
|
||||
back: ClickChains,
|
||||
forward: ClickChains,
|
||||
}
|
||||
|
||||
impl ClickTracker {
|
||||
fn input(&mut self, position: PhysicalPosition<f64>, button: MouseButton, state: ElementState) -> u32 {
|
||||
let position = (position.x as i32, position.y as i32);
|
||||
let clicks = match button {
|
||||
MouseButton::Left => &mut self.left,
|
||||
MouseButton::Right => &mut self.right,
|
||||
MouseButton::Middle => &mut self.middle,
|
||||
MouseButton::Back => &mut self.back,
|
||||
MouseButton::Forward => &mut self.forward,
|
||||
_ => return 1,
|
||||
};
|
||||
let chain = match state {
|
||||
ElementState::Pressed => &mut clicks.down,
|
||||
ElementState::Released => &mut clicks.up,
|
||||
};
|
||||
|
||||
let now = Instant::now();
|
||||
let count = match chain {
|
||||
Some(previous) => {
|
||||
let within_time = now.saturating_duration_since(previous.time) <= MULTICLICK_TIMEOUT;
|
||||
let dx = position.0.abs_diff(previous.position.0) as usize;
|
||||
let dy = position.1.abs_diff(previous.position.1) as usize;
|
||||
let within_distance = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
|
||||
if within_time && within_distance { previous.count.saturating_add(1) } else { 1 }
|
||||
}
|
||||
None => 1,
|
||||
};
|
||||
*chain = Some(Click { time: now, position, count });
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClickChains {
|
||||
down: Option<Click>,
|
||||
up: Option<Click>,
|
||||
}
|
||||
|
||||
struct Click {
|
||||
time: Instant,
|
||||
position: (i32, i32),
|
||||
count: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShakeTracker {
|
||||
samples: VecDeque<(PhysicalPosition<f64>, Duration)>,
|
||||
last: Option<Duration>,
|
||||
}
|
||||
|
||||
// Keep heuristic and constants in sync with `detectShake` in `frontend/src/utility-functions/input.ts`.
|
||||
impl ShakeTracker {
|
||||
const SENSITIVITY_DIRECTION_CHANGES: usize = 3;
|
||||
const SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO: f64 = 0.1;
|
||||
const DETECTION_WINDOW: Duration = Duration::from_millis(500);
|
||||
const DEBOUNCE: Duration = Duration::from_millis(1000);
|
||||
|
||||
fn detect(&mut self, position: PhysicalPosition<f64>, now: Duration) -> bool {
|
||||
self.samples.push_back((position, now));
|
||||
|
||||
while self.samples.front().is_some_and(|&(_, time)| now.saturating_sub(time) > Self::DETECTION_WINDOW) {
|
||||
self.samples.pop_front();
|
||||
}
|
||||
|
||||
if self.samples.len() <= 3 || self.last.is_some_and(|time| now.saturating_sub(time) <= Self::DEBOUNCE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut total_distance_squared = 0.;
|
||||
for i in 1..self.samples.len() {
|
||||
let (p1, _) = self.samples[i - 1];
|
||||
let (p2, _) = self.samples[i];
|
||||
total_distance_squared += (p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2);
|
||||
}
|
||||
|
||||
let mut direction_changes = 0;
|
||||
let mut average = PhysicalPosition::new(0., 0.);
|
||||
let interior_count = self.samples.len() - 2;
|
||||
for i in 0..interior_count {
|
||||
let (p1, _) = self.samples[i];
|
||||
let (p2, _) = self.samples[i + 1];
|
||||
let (p3, _) = self.samples[i + 2];
|
||||
|
||||
let vector1 = (p2.x - p1.x, p2.y - p1.y);
|
||||
let vector2 = (p3.x - p2.x, p3.y - p2.y);
|
||||
if vector1.0 * vector2.0 + vector1.1 * vector2.1 < 0. {
|
||||
direction_changes += 1;
|
||||
}
|
||||
|
||||
average.x += p2.x;
|
||||
average.y += p2.y;
|
||||
}
|
||||
average.x /= interior_count as f64;
|
||||
average.y /= interior_count as f64;
|
||||
|
||||
let (last, _) = self.samples[self.samples.len() - 1];
|
||||
let displacement_squared = (last.x - average.x).powi(2) + (last.y - average.y).powi(2);
|
||||
|
||||
if Self::SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO * total_distance_squared >= displacement_squared && direction_changes >= Self::SENSITIVITY_DIRECTION_CHANGES {
|
||||
self.last = Some(now);
|
||||
self.samples.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod cli;
|
||||
mod dirs;
|
||||
mod event;
|
||||
mod gpu_context;
|
||||
mod input;
|
||||
mod persist;
|
||||
mod preferences;
|
||||
mod render;
|
||||
|
||||
@@ -52,13 +52,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return srgb_to_linear(immediates.background_color);
|
||||
}
|
||||
|
||||
let ui_linear = srgb_to_linear(textureSample(t_ui, s_diffuse, ui_coordinate));
|
||||
if (ui_linear.a >= 0.999) {
|
||||
return ui_linear;
|
||||
let ui_sample = textureSample(t_ui, s_diffuse, ui_coordinate);
|
||||
if (ui_sample.a >= 0.999) {
|
||||
return srgb_to_linear(ui_sample);
|
||||
}
|
||||
|
||||
// UI texture is premultiplied, we need to unpremultiply before blending
|
||||
let ui_srgb = linear_to_srgb(unpremultiply(ui_linear));
|
||||
let ui_srgb = unpremultiply(ui_sample);
|
||||
|
||||
let viewport_coordinate = (in.tex_coords - immediates.viewport_offset) * immediates.viewport_scale;
|
||||
if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 ||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use wgpu::PresentMode;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::wrapper::{WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface};
|
||||
use crate::wrapper::{Texture, WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface};
|
||||
|
||||
#[derive(derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
@@ -10,14 +10,14 @@ pub(crate) struct RenderState {
|
||||
executor: WgpuExecutor,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
render_pipeline: wgpu::RenderPipeline,
|
||||
transparent_texture: std::sync::Arc<wgpu::Texture>,
|
||||
transparent_texture: Texture,
|
||||
sampler: wgpu::Sampler,
|
||||
desired_width: u32,
|
||||
desired_height: u32,
|
||||
viewport_scale: [f32; 2],
|
||||
viewport_offset: [f32; 2],
|
||||
viewport_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
||||
overlays_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
||||
viewport_texture: Option<Texture>,
|
||||
overlays_texture: Option<Texture>,
|
||||
ui_texture: Option<wgpu::Texture>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
#[derivative(Debug = "ignore")]
|
||||
@@ -46,8 +46,8 @@ impl RenderState {
|
||||
|
||||
surface.configure(&context.device, &config);
|
||||
|
||||
let transparent_texture = std::sync::Arc::new(context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Transparent Texture"),
|
||||
let transparent_texture = Texture::from(context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("transparent_fallback"),
|
||||
size: wgpu::Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
@@ -193,7 +193,7 @@ impl RenderState {
|
||||
self.surface_outdated = true;
|
||||
}
|
||||
|
||||
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc<wgpu::Texture>) {
|
||||
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: Texture) {
|
||||
self.viewport_texture = Some(viewport_texture);
|
||||
self.update_bindgroup();
|
||||
}
|
||||
@@ -231,7 +231,7 @@ impl RenderState {
|
||||
let result = self.executor.render_vello_scene(&scene, size, &Default::default(), None);
|
||||
match result {
|
||||
Ok(texture) => {
|
||||
self.overlays_texture = Some(texture.into());
|
||||
self.overlays_texture = Some(texture);
|
||||
}
|
||||
Err(e) => {
|
||||
self.overlays_texture = None;
|
||||
|
||||
@@ -344,6 +344,14 @@ unsafe fn position_helper(main: HWND, helper: HWND) {
|
||||
let h = (r.bottom - r.top) + RESIZE_BAND_THICKNESS * 2;
|
||||
|
||||
let _ = unsafe { SetWindowPos(helper, Some(main), x, y, w, h, SWP_NOACTIVATE | SWP_NOSENDCHANGING) };
|
||||
|
||||
let outer = unsafe { CreateRectRgn(0, 0, w, h) };
|
||||
let inner = unsafe { CreateRectRgn(RESIZE_BAND_THICKNESS, RESIZE_BAND_THICKNESS, w - RESIZE_BAND_THICKNESS, h - RESIZE_BAND_THICKNESS) };
|
||||
let _ = unsafe { CombineRgn(Some(outer), Some(outer), Some(inner), RGN_DIFF) };
|
||||
let _ = unsafe { DeleteObject(inner.into()) };
|
||||
if unsafe { SetWindowRgn(helper, Some(outer), false) } == 0 {
|
||||
let _ = unsafe { DeleteObject(outer.into()) };
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
||||
|
||||
Reference in New Issue
Block a user