#![warn(missing_docs)]
use std::collections::HashMap;
#[cfg(feature = "clipboard")]
use copypasta::{ClipboardContext, ClipboardProvider};
use egui::{
emath::{pos2, vec2},
Context, Key, Pos2,
};
use winit::{
dpi::PhysicalSize,
event::{Event, ModifiersState, TouchPhase, VirtualKeyCode, VirtualKeyCode::*, WindowEvent::*},
window::CursorIcon,
};
#[derive(Debug, Default)]
pub struct PlatformDescriptor {
pub physical_width: u32,
pub physical_height: u32,
pub scale_factor: f64,
pub font_definitions: egui::FontDefinitions,
pub style: egui::Style,
}
#[cfg(feature = "webbrowser")]
fn handle_links(output: &egui::PlatformOutput) {
if let Some(open_url) = &output.open_url {
if let Err(err) = webbrowser::open(&open_url.url) {
eprintln!("Failed to open url: {}", err);
}
}
}
#[cfg(feature = "clipboard")]
fn handle_clipboard(output: &egui::PlatformOutput, clipboard: Option<&mut ClipboardContext>) {
if !output.copied_text.is_empty() {
if let Some(clipboard) = clipboard {
if let Err(err) = clipboard.set_contents(output.copied_text.clone()) {
eprintln!("Copy/Cut error: {}", err);
}
}
}
}
pub struct Platform {
scale_factor: f64,
context: Context,
raw_input: egui::RawInput,
modifier_state: ModifiersState,
pointer_pos: Option<egui::Pos2>,
#[cfg(feature = "clipboard")]
clipboard: Option<ClipboardContext>,
touch_pointer_pressed: u32,
device_indices: HashMap<winit::event::DeviceId, u64>,
next_device_index: u64,
}
impl Platform {
pub fn new(descriptor: PlatformDescriptor) -> Self {
let context = Context::default();
context.set_fonts(descriptor.font_definitions.clone());
context.set_style(descriptor.style);
let raw_input = egui::RawInput {
pixels_per_point: Some(descriptor.scale_factor as f32),
screen_rect: Some(egui::Rect::from_min_size(
Pos2::default(),
vec2(
descriptor.physical_width as f32,
descriptor.physical_height as f32,
) / descriptor.scale_factor as f32,
)),
..Default::default()
};
Self {
scale_factor: descriptor.scale_factor,
context,
raw_input,
modifier_state: winit::event::ModifiersState::empty(),
pointer_pos: Some(Pos2::default()),
#[cfg(feature = "clipboard")]
clipboard: ClipboardContext::new().ok(),
touch_pointer_pressed: 0,
device_indices: HashMap::new(),
next_device_index: 1,
}
}
pub fn handle_event<T>(&mut self, winit_event: &Event<T>) {
match winit_event {
Event::WindowEvent {
window_id: _window_id,
event,
} => match event {
Resized(PhysicalSize {
width: 0,
height: 0,
}) => {}
Resized(physical_size) => {
self.raw_input.screen_rect = Some(egui::Rect::from_min_size(
Default::default(),
vec2(physical_size.width as f32, physical_size.height as f32)
/ self.scale_factor as f32,
));
}
ScaleFactorChanged {
scale_factor,
new_inner_size,
} => {
self.scale_factor = *scale_factor;
self.raw_input.pixels_per_point = Some(*scale_factor as f32);
self.raw_input.screen_rect = Some(egui::Rect::from_min_size(
Default::default(),
vec2(new_inner_size.width as f32, new_inner_size.height as f32)
/ self.scale_factor as f32,
));
}
MouseInput { state, button, .. } => {
if let winit::event::MouseButton::Other(..) = button {
} else {
if let Some(pointer_pos) = self.pointer_pos {
self.raw_input.events.push(egui::Event::PointerButton {
pos: pointer_pos,
button: match button {
winit::event::MouseButton::Left => egui::PointerButton::Primary,
winit::event::MouseButton::Right => {
egui::PointerButton::Secondary
}
winit::event::MouseButton::Middle => {
egui::PointerButton::Middle
}
winit::event::MouseButton::Other(_) => unreachable!(),
},
pressed: *state == winit::event::ElementState::Pressed,
modifiers: Default::default(),
});
}
}
}
Touch(touch) => {
let pointer_pos = pos2(
touch.location.x as f32 / self.scale_factor as f32,
touch.location.y as f32 / self.scale_factor as f32,
);
let device_id = match self.device_indices.get(&touch.device_id) {
Some(id) => *id,
None => {
let device_id = self.next_device_index;
self.device_indices.insert(touch.device_id, device_id);
self.next_device_index += 1;
device_id
}
};
let egui_phase = match touch.phase {
TouchPhase::Started => egui::TouchPhase::Start,
TouchPhase::Moved => egui::TouchPhase::Move,
TouchPhase::Ended => egui::TouchPhase::End,
TouchPhase::Cancelled => egui::TouchPhase::Cancel,
};
let force = match touch.force {
Some(winit::event::Force::Calibrated { force, .. }) => force as f32,
Some(winit::event::Force::Normalized(force)) => force as f32,
None => 0.0f32, };
self.raw_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(device_id),
id: egui::TouchId(touch.id),
phase: egui_phase,
pos: pointer_pos,
force,
});
let was_pressed = self.touch_pointer_pressed > 0;
match touch.phase {
TouchPhase::Started => {
self.touch_pointer_pressed += 1;
}
TouchPhase::Ended | TouchPhase::Cancelled => {
self.touch_pointer_pressed = match self
.touch_pointer_pressed
.checked_sub(1)
{
Some(count) => count,
None => {
eprintln!("Pointer emulation error: Unbalanced touch start/stop events from Winit");
0
}
};
}
TouchPhase::Moved => {
self.raw_input
.events
.push(egui::Event::PointerMoved(pointer_pos));
}
}
if !was_pressed && self.touch_pointer_pressed > 0 {
self.raw_input.events.push(egui::Event::PointerButton {
pos: pointer_pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: Default::default(),
});
} else if was_pressed && self.touch_pointer_pressed == 0 {
self.raw_input.events.push(egui::Event::PointerButton {
pos: pointer_pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: Default::default(),
});
self.raw_input.events.push(egui::Event::PointerGone);
}
}
MouseWheel { delta, .. } => {
let mut delta = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => {
let line_height = 8.0; vec2(*x, *y) * line_height
}
winit::event::MouseScrollDelta::PixelDelta(delta) => {
vec2(delta.x as f32, delta.y as f32)
}
};
if cfg!(target_os = "macos") {
delta.x *= -1.0;
}
if self.raw_input.modifiers.ctrl || self.raw_input.modifiers.command {
self.raw_input
.events
.push(egui::Event::Zoom((delta.y / 200.0).exp()));
} else {
self.raw_input.events.push(egui::Event::Scroll(delta));
}
}
CursorMoved { position, .. } => {
let pointer_pos = pos2(
position.x as f32 / self.scale_factor as f32,
position.y as f32 / self.scale_factor as f32,
);
self.pointer_pos = Some(pointer_pos);
self.raw_input
.events
.push(egui::Event::PointerMoved(pointer_pos));
}
CursorLeft { .. } => {
self.pointer_pos = None;
self.raw_input.events.push(egui::Event::PointerGone);
}
ModifiersChanged(input) => {
self.modifier_state = *input;
self.raw_input.modifiers = winit_to_egui_modifiers(*input);
}
KeyboardInput { input, .. } => {
if let Some(virtual_keycode) = input.virtual_keycode {
let pressed = input.state == winit::event::ElementState::Pressed;
let ctrl = self.modifier_state.ctrl();
match (pressed, ctrl, virtual_keycode) {
(true, true, VirtualKeyCode::C) => {
self.raw_input.events.push(egui::Event::Copy)
}
(true, true, VirtualKeyCode::X) => {
self.raw_input.events.push(egui::Event::Cut)
}
(true, true, VirtualKeyCode::V) => {
#[cfg(feature = "clipboard")]
if let Some(ref mut clipboard) = self.clipboard {
if let Ok(contents) = clipboard.get_contents() {
self.raw_input.events.push(egui::Event::Text(contents))
}
}
}
_ => {
if let Some(key) = winit_to_egui_key_code(virtual_keycode) {
self.raw_input.events.push(egui::Event::Key {
key,
pressed,
modifiers: winit_to_egui_modifiers(self.modifier_state),
});
}
}
}
}
}
ReceivedCharacter(ch) => {
if is_printable(*ch)
&& !self.modifier_state.ctrl()
&& !self.modifier_state.logo()
{
self.raw_input
.events
.push(egui::Event::Text(ch.to_string()));
}
}
_ => {}
},
Event::DeviceEvent { .. } => {}
_ => {}
}
}
pub fn captures_event<T>(&self, winit_event: &Event<T>) -> bool {
match winit_event {
Event::WindowEvent {
window_id: _window_id,
event,
} => match event {
ReceivedCharacter(_) | KeyboardInput { .. } | ModifiersChanged(_) => {
self.context().wants_keyboard_input()
}
MouseWheel { .. } | MouseInput { .. } => self.context().wants_pointer_input(),
CursorMoved { .. } => self.context().is_using_pointer(),
Touch { .. } => self.context().is_using_pointer(),
_ => false,
},
_ => false,
}
}
pub fn update_time(&mut self, elapsed_seconds: f64) {
self.raw_input.time = Some(elapsed_seconds);
}
pub fn begin_frame(&mut self) {
self.context.begin_frame(self.raw_input.take());
}
pub fn end_frame(&mut self, window: Option<&winit::window::Window>) -> egui::FullOutput {
#[allow(clippy::let_and_return)]
let output = self.context.end_frame();
if let Some(window) = window {
if let Some(cursor_icon) = egui_to_winit_cursor_icon(output.platform_output.cursor_icon)
{
window.set_cursor_visible(true);
if self.pointer_pos.is_some() {
window.set_cursor_icon(cursor_icon);
}
} else {
window.set_cursor_visible(false);
}
}
#[cfg(feature = "clipboard")]
handle_clipboard(&output.platform_output, self.clipboard.as_mut());
#[cfg(feature = "webbrowser")]
handle_links(&output.platform_output);
output
}
pub fn context(&self) -> Context {
self.context.clone()
}
pub fn raw_input_mut(&mut self) -> &mut egui::RawInput {
&mut self.raw_input
}
}
#[inline]
fn winit_to_egui_key_code(key: VirtualKeyCode) -> Option<egui::Key> {
Some(match key {
Escape => Key::Escape,
Insert => Key::Insert,
Home => Key::Home,
Delete => Key::Delete,
End => Key::End,
PageDown => Key::PageDown,
PageUp => Key::PageUp,
Left => Key::ArrowLeft,
Up => Key::ArrowUp,
Right => Key::ArrowRight,
Down => Key::ArrowDown,
Back => Key::Backspace,
Return => Key::Enter,
Tab => Key::Tab,
Space => Key::Space,
Key1 => Key::Num1,
Key2 => Key::Num2,
Key3 => Key::Num3,
Key4 => Key::Num4,
Key5 => Key::Num5,
Key6 => Key::Num6,
Key7 => Key::Num7,
Key8 => Key::Num8,
Key9 => Key::Num9,
Key0 => Key::Num0,
A => Key::A,
B => Key::B,
C => Key::C,
D => Key::D,
E => Key::E,
F => Key::F,
G => Key::G,
H => Key::H,
I => Key::I,
J => Key::J,
K => Key::K,
L => Key::L,
M => Key::M,
N => Key::N,
O => Key::O,
P => Key::P,
Q => Key::Q,
R => Key::R,
S => Key::S,
T => Key::T,
U => Key::U,
V => Key::V,
W => Key::W,
X => Key::X,
Y => Key::Y,
Z => Key::Z,
_ => {
return None;
}
})
}
#[inline]
fn winit_to_egui_modifiers(modifiers: ModifiersState) -> egui::Modifiers {
egui::Modifiers {
alt: modifiers.alt(),
ctrl: modifiers.ctrl(),
shift: modifiers.shift(),
#[cfg(target_os = "macos")]
mac_cmd: modifiers.logo(),
#[cfg(target_os = "macos")]
command: modifiers.logo(),
#[cfg(not(target_os = "macos"))]
mac_cmd: false,
#[cfg(not(target_os = "macos"))]
command: modifiers.ctrl(),
}
}
#[inline]
fn egui_to_winit_cursor_icon(icon: egui::CursorIcon) -> Option<winit::window::CursorIcon> {
use egui::CursorIcon::*;
match icon {
Default => Some(CursorIcon::Default),
ContextMenu => Some(CursorIcon::ContextMenu),
Help => Some(CursorIcon::Help),
PointingHand => Some(CursorIcon::Hand),
Progress => Some(CursorIcon::Progress),
Wait => Some(CursorIcon::Wait),
Cell => Some(CursorIcon::Cell),
Crosshair => Some(CursorIcon::Crosshair),
Text => Some(CursorIcon::Text),
VerticalText => Some(CursorIcon::VerticalText),
Alias => Some(CursorIcon::Alias),
Copy => Some(CursorIcon::Copy),
Move => Some(CursorIcon::Move),
NoDrop => Some(CursorIcon::NoDrop),
NotAllowed => Some(CursorIcon::NotAllowed),
Grab => Some(CursorIcon::Grab),
Grabbing => Some(CursorIcon::Grabbing),
AllScroll => Some(CursorIcon::AllScroll),
ResizeHorizontal => Some(CursorIcon::EwResize),
ResizeNeSw => Some(CursorIcon::NeswResize),
ResizeNwSe => Some(CursorIcon::NwseResize),
ResizeVertical => Some(CursorIcon::NsResize),
ResizeEast => Some(CursorIcon::EResize),
ResizeSouthEast => Some(CursorIcon::SeResize),
ResizeSouth => Some(CursorIcon::SResize),
ResizeSouthWest => Some(CursorIcon::SwResize),
ResizeWest => Some(CursorIcon::WResize),
ResizeNorthWest => Some(CursorIcon::NwResize),
ResizeNorth => Some(CursorIcon::NResize),
ResizeNorthEast => Some(CursorIcon::NeResize),
ResizeColumn => Some(CursorIcon::ColResize),
ResizeRow => Some(CursorIcon::RowResize),
ZoomIn => Some(CursorIcon::ZoomIn),
ZoomOut => Some(CursorIcon::ZoomOut),
None => Option::None,
}
}
#[inline]
fn is_printable(chr: char) -> bool {
let is_in_private_use_area = ('\u{e000}'..='\u{f8ff}').contains(&chr)
|| ('\u{f0000}'..='\u{ffffd}').contains(&chr)
|| ('\u{100000}'..='\u{10fffd}').contains(&chr);
!is_in_private_use_area && !chr.is_ascii_control()
}