use std::cell::{Cell, RefCell};
use std::time::Instant;
use baseview::dpi::{LogicalPosition, LogicalSize, Size};
use baseview::{
Event, EventStatus, Window, WindowContext, WindowHandle, WindowHandler, WindowOpenOptions,
WindowScalePolicy, WindowSize,
};
use copypasta::ClipboardProvider;
use egui::{FullOutput, Pos2, Rect, Rgba, ViewportCommand, ViewportOutput, pos2, vec2};
use keyboard_types::Modifiers;
use raw_window_handle::HasWindowHandle;
use crate::{GraphicsConfig, renderer::Renderer};
#[cfg(feature = "nice-log")]
use nice_plug_core::{nice_error as error, nice_warn as warn};
#[cfg(all(feature = "tracing", not(feature = "nice-log")))]
use tracing::{error, warn};
#[derive(Debug, Clone)]
pub struct EguiWindowSettings {
pub title: String,
pub size: Size,
pub scale_policy: WindowScalePolicy,
pub graphics: GraphicsConfig,
}
impl EguiWindowSettings {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn with_tile(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[inline]
pub fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
#[inline]
pub fn with_scale_policy(mut self, scale_policy: impl Into<WindowScalePolicy>) -> Self {
self.scale_policy = scale_policy.into();
self
}
#[inline]
pub fn with_graphics_config(mut self, config: GraphicsConfig) -> Self {
self.graphics = config;
self
}
}
impl Default for EguiWindowSettings {
fn default() -> Self {
Self {
title: String::new(),
size: Size::Logical(LogicalSize {
width: 300.0,
height: 200.0,
}),
scale_policy: WindowScalePolicy::default(),
graphics: GraphicsConfig::default(),
}
}
}
pub struct ExtraOutputCommands {
clear_color: Option<Rgba>,
key_capture: Option<KeyCapture>,
}
impl ExtraOutputCommands {
pub(crate) fn new() -> Self {
Self {
clear_color: None,
key_capture: None,
}
}
pub fn clear_color(&mut self, clear_color: Rgba) {
self.clear_color = Some(clear_color);
}
pub fn set_key_capture(&mut self, key_capture: KeyCapture) {
self.key_capture = Some(key_capture);
}
}
#[derive(Default, Debug, Clone, PartialEq)]
pub enum KeyCapture {
#[default]
CaptureAll,
IgnoreAll,
CaptureKeys(Vec<keyboard_types::Key>),
IgnoreKeys(Vec<keyboard_types::Key>),
}
struct EguiWindowInner<State, U, O>
where
State: 'static + Send,
U: FnMut(&mut egui::Ui, &mut ExtraOutputCommands, &mut State),
U: 'static + Send,
O: FnMut(&FullOutput, &ViewportOutput, &mut State),
O: 'static + Send,
{
user_state: State,
user_update: U,
user_output: O,
egui_ctx: egui::Context,
clipboard_ctx: Option<copypasta::ClipboardContext>,
renderer: Renderer,
clear_color: Rgba,
key_capture: KeyCapture,
}
pub struct EguiWindow<State, U, O>
where
State: 'static + Send,
U: FnMut(&mut egui::Ui, &mut ExtraOutputCommands, &mut State),
U: 'static + Send,
O: FnMut(&FullOutput, &ViewportOutput, &mut State),
O: 'static + Send,
{
inner: RefCell<EguiWindowInner<State, U, O>>,
egui_input: RefCell<egui::RawInput>,
viewport_id: egui::ViewportId,
start_time: Instant,
scale_factor: Cell<f64>,
pointer_logical_pos: Cell<Option<egui::Pos2>>,
current_cursor_icon: Cell<baseview::MouseCursor>,
repaint_after: Cell<Option<Instant>>,
pub window: WindowContext,
}
impl<State, U, O> EguiWindow<State, U, O>
where
State: 'static + Send,
U: FnMut(&mut egui::Ui, &mut ExtraOutputCommands, &mut State),
U: 'static + Send,
O: FnMut(&FullOutput, &ViewportOutput, &mut State),
O: 'static + Send,
{
fn new<B>(
window: WindowContext,
title: String,
graphics_config: GraphicsConfig,
mut build: B,
output: O,
update: U,
mut state: State,
) -> EguiWindow<State, U, O>
where
B: FnMut(&egui::Context, &mut ExtraOutputCommands, &mut State),
B: 'static + Send,
{
let renderer = Renderer::new(window.clone(), graphics_config).unwrap_or_else(|err| {
error!("oops! the gpu backend couldn't initialize! \n {err}");
panic!("gpu backend failed to initialize: \n {err}")
});
let egui_ctx = egui::Context::default();
let size = window.size();
let screen_rect = logical_screen_rect(size);
let scale_factor = size.scale_factor;
let viewport_info = egui::ViewportInfo {
parent: None,
title: Some(title),
native_pixels_per_point: Some(scale_factor as f32),
focused: Some(true),
inner_rect: Some(screen_rect),
..Default::default()
};
let viewport_id = egui::ViewportId::default();
let mut egui_input = egui::RawInput {
max_texture_side: Some(renderer.max_texture_side()),
screen_rect: Some(screen_rect),
..Default::default()
};
let _ = egui_input.viewports.insert(viewport_id, viewport_info);
let mut commands = ExtraOutputCommands::new();
(build)(&egui_ctx, &mut commands, &mut state);
let clipboard_ctx = match copypasta::ClipboardContext::new() {
Ok(clipboard_ctx) => Some(clipboard_ctx),
Err(e) => {
error!("Failed to initialize clipboard: {}", e);
None
}
};
let start_time = Instant::now();
Self {
inner: RefCell::new(EguiWindowInner {
user_state: state,
user_update: update,
user_output: output,
egui_ctx,
clipboard_ctx,
renderer,
clear_color: commands.clear_color.unwrap_or(Rgba::BLACK),
key_capture: commands.key_capture.unwrap_or_default(),
}),
viewport_id,
start_time,
egui_input: egui_input.into(),
pointer_logical_pos: None.into(),
current_cursor_icon: baseview::MouseCursor::Default.into(),
scale_factor: scale_factor.into(),
repaint_after: Some(start_time).into(),
window,
}
}
pub fn open_parented<P, B>(
parent: &P,
settings: EguiWindowSettings,
state: State,
build: B,
output: O,
update: U,
) -> WindowHandle
where
P: HasWindowHandle,
B: FnMut(&egui::Context, &mut ExtraOutputCommands, &mut State),
B: 'static + Send,
{
let options = WindowOpenOptions::new()
.with_title(settings.title.clone())
.with_size(settings.size)
.with_scale_policy(settings.scale_policy);
#[cfg(feature = "opengl")]
let options = { options.with_gl_config(Some(settings.graphics.gl_config.clone())) };
Window::open_parented(parent, options, move |window| -> EguiWindow<State, U, O> {
EguiWindow::new(
window,
settings.title,
settings.graphics,
build,
output,
update,
state,
)
})
}
pub fn open_blocking<B>(
settings: EguiWindowSettings,
state: State,
build: B,
output: O,
update: U,
) where
B: FnMut(&egui::Context, &mut ExtraOutputCommands, &mut State),
B: 'static + Send,
{
let options = WindowOpenOptions::new()
.with_title(settings.title.clone())
.with_size(settings.size)
.with_scale_policy(settings.scale_policy);
#[cfg(feature = "opengl")]
let options = { options.with_gl_config(Some(settings.graphics.gl_config.clone())) };
Window::open_blocking(options, move |window| -> EguiWindow<State, U, O> {
EguiWindow::new(
window,
settings.title,
settings.graphics,
build,
output,
update,
state,
)
})
}
fn update_modifiers(&self, modifiers: &Modifiers) {
let mut egui_input = self.egui_input.borrow_mut();
egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty();
egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty();
egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty();
}
}
impl<State, U, O> WindowHandler for EguiWindow<State, U, O>
where
State: 'static + Send,
U: FnMut(&mut egui::Ui, &mut ExtraOutputCommands, &mut State),
U: 'static + Send,
O: FnMut(&FullOutput, &ViewportOutput, &mut State),
O: 'static + Send,
{
fn on_frame(&self) {
let egui_input = {
let mut egui_input = self.egui_input.borrow_mut();
egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
egui_input.screen_rect = Some(logical_screen_rect(self.window.size()));
egui_input.take()
};
let mut full_output = {
let mut extra_commands = ExtraOutputCommands::new();
let mut inner = self.inner.borrow_mut();
let EguiWindowInner {
user_state,
user_update,
user_output,
egui_ctx,
clipboard_ctx: _,
renderer: _,
clear_color,
key_capture,
} = &mut *inner;
let output = egui_ctx.run_ui(egui_input, |ui| {
user_update(ui, &mut extra_commands, user_state)
});
if let Some(c) = extra_commands.clear_color.take() {
*clear_color = c;
}
if let Some(k) = extra_commands.key_capture.take() {
*key_capture = k;
}
if let Some(viewport_output) = output.viewport_output.get(&self.viewport_id) {
user_output(&output, viewport_output, user_state);
}
output
};
let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
self.window.request_close();
return;
};
for command in viewport_output.commands.iter() {
match command {
ViewportCommand::Close => {
self.window.request_close();
}
ViewportCommand::InnerSize(size) => self.window.resize(LogicalSize {
width: size.x.max(1.0),
height: size.y.max(1.0),
}),
ViewportCommand::Focus => {
self.window.focus();
}
_ => {}
}
}
{
let mut inner = self.inner.borrow_mut();
let EguiWindowInner {
user_state: _,
user_update: _,
user_output: _,
egui_ctx,
clipboard_ctx,
renderer,
clear_color: bg_color,
key_capture: _,
} = &mut *inner;
let now = Instant::now();
let do_repaint_now = if let Some(t) = self.repaint_after.get() {
now >= t || viewport_output.repaint_delay.is_zero()
} else {
viewport_output.repaint_delay.is_zero()
};
if do_repaint_now {
let size = self.window.size();
renderer.render(
&self.window,
*bg_color,
size.physical,
size.scale_factor as f32,
egui_ctx,
&mut full_output,
);
self.repaint_after.set(None);
} else if let Some(t) = now.checked_add(viewport_output.repaint_delay) {
self.repaint_after.set(Some(t));
}
for command in full_output.platform_output.commands {
match command {
egui::OutputCommand::CopyText(text) => {
if let Some(clipboard_ctx) = clipboard_ctx.as_mut()
&& let Err(err) = clipboard_ctx.set_contents(text)
{
error!("Copy/Cut error: {}", err);
}
}
egui::OutputCommand::CopyImage(_) => {
warn!("Copying images is not supported in egui_baseview.");
}
egui::OutputCommand::OpenUrl(open_url) => {
if let Err(err) = open::that_detached(&open_url.url) {
error!("Open error: {}", err);
}
}
}
}
}
let cursor_icon =
crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
if self.current_cursor_icon.get() != cursor_icon {
self.current_cursor_icon.set(cursor_icon);
self.window.set_mouse_cursor(cursor_icon);
}
#[cfg(feature = "keyboard_focus_workaround")]
{
if !full_output.platform_output.events.is_empty()
|| full_output.platform_output.ime.is_some()
{
window.focus();
}
}
}
fn resized(&self, new_size: WindowSize) {
let screen_rect = logical_screen_rect(new_size);
let mut egui_input = self.egui_input.borrow_mut();
egui_input.screen_rect = Some(screen_rect);
let viewport_info = egui_input.viewports.get_mut(&self.viewport_id).unwrap();
viewport_info.native_pixels_per_point = Some(new_size.scale_factor as f32);
viewport_info.inner_rect = Some(screen_rect);
self.repaint_after.set(Some(Instant::now()));
self.scale_factor.set(new_size.scale_factor);
}
fn on_event(&self, event: Event) -> EventStatus {
let mut return_status = EventStatus::Captured;
if matches!(
event,
baseview::Event::Mouse(baseview::MouseEvent::ButtonPressed { .. })
) && !self.window.has_focus()
{
self.window.focus();
}
match &event {
baseview::Event::Mouse(event) => match event {
baseview::MouseEvent::CursorMoved {
position,
modifiers,
} => {
self.update_modifiers(modifiers);
let logical_pos: LogicalPosition<f32> =
position.to_logical(self.scale_factor.get());
let pos = pos2(logical_pos.x, logical_pos.y);
self.pointer_logical_pos.set(Some(pos));
self.egui_input
.borrow_mut()
.events
.push(egui::Event::PointerMoved(pos));
}
baseview::MouseEvent::ButtonPressed { button, modifiers } => {
self.update_modifiers(modifiers);
if let Some(pos) = self.pointer_logical_pos.get()
&& let Some(button) = crate::translate::translate_mouse_button(*button)
{
let mut egui_input = self.egui_input.borrow_mut();
let modifiers = egui_input.modifiers;
egui_input.events.push(egui::Event::PointerButton {
pos,
button,
pressed: true,
modifiers,
});
}
}
baseview::MouseEvent::ButtonReleased { button, modifiers } => {
self.update_modifiers(modifiers);
if let Some(pos) = self.pointer_logical_pos.get()
&& let Some(button) = crate::translate::translate_mouse_button(*button)
{
let mut egui_input = self.egui_input.borrow_mut();
let modifiers = egui_input.modifiers;
egui_input.events.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
}
}
baseview::MouseEvent::WheelScrolled {
delta: scroll_delta,
modifiers,
} => {
self.update_modifiers(modifiers);
#[allow(unused_mut)]
let (unit, mut delta) = match scroll_delta {
baseview::ScrollDelta::Lines { x, y } => {
(egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
}
baseview::ScrollDelta::Pixels { x, y } => (
egui::MouseWheelUnit::Point,
egui::vec2(*x, *y) * self.window.scale_factor() as f32,
),
};
if cfg!(target_os = "macos") {
delta.x *= -1.0;
}
let mut egui_input = self.egui_input.borrow_mut();
let modifiers = egui_input.modifiers;
egui_input.events.push(egui::Event::MouseWheel {
unit,
delta,
modifiers,
phase: egui::TouchPhase::Move,
});
}
baseview::MouseEvent::CursorLeft => {
self.pointer_logical_pos.set(None);
self.egui_input
.borrow_mut()
.events
.push(egui::Event::PointerGone);
}
_ => {}
},
baseview::Event::Keyboard(event) => {
use keyboard_types::Code;
let pressed = event.state == keyboard_types::KeyState::Down;
let mut egui_input = self.egui_input.borrow_mut();
match event.code {
Code::ShiftLeft | Code::ShiftRight => egui_input.modifiers.shift = pressed,
Code::ControlLeft | Code::ControlRight => {
egui_input.modifiers.ctrl = pressed;
#[cfg(not(target_os = "macos"))]
{
egui_input.modifiers.command = pressed;
}
}
Code::AltLeft | Code::AltRight => egui_input.modifiers.alt = pressed,
Code::MetaLeft | Code::MetaRight => {
#[cfg(target_os = "macos")]
{
egui_input.modifiers.mac_cmd = pressed;
egui_input.modifiers.command = pressed;
}
}
_ => (),
}
if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
let modifiers = egui_input.modifiers;
egui_input.events.push(egui::Event::Key {
key,
physical_key: None,
pressed,
repeat: event.repeat,
modifiers,
});
}
if pressed {
if is_cut_command(egui_input.modifiers, event.code) {
egui_input.events.push(egui::Event::Cut);
} else if is_copy_command(egui_input.modifiers, event.code) {
egui_input.events.push(egui::Event::Copy);
} else if is_paste_command(egui_input.modifiers, event.code) {
if let Some(clipboard_ctx) = self.inner.borrow_mut().clipboard_ctx.as_mut()
{
match clipboard_ctx.get_contents() {
Ok(contents) => egui_input.events.push(egui::Event::Text(contents)),
Err(err) => {
error!("Paste error: {}", err);
}
}
}
} else if let keyboard_types::Key::Character(written) = &event.key
&& !egui_input.modifiers.ctrl
&& !egui_input.modifiers.command
{
egui_input.events.push(egui::Event::Text(written.clone()));
}
}
match &self.inner.borrow().key_capture {
KeyCapture::CaptureAll => {}
KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
KeyCapture::CaptureKeys(keys) => {
if !keys.contains(&event.key) {
return_status = EventStatus::Ignored
}
}
KeyCapture::IgnoreKeys(keys) => {
if keys.contains(&event.key) {
return_status = EventStatus::Ignored
}
}
}
}
baseview::Event::Window(event) => match event {
baseview::WindowEvent::Focused => {
let mut egui_input = self.egui_input.borrow_mut();
egui_input.events.push(egui::Event::WindowFocused(true));
egui_input
.viewports
.get_mut(&self.viewport_id)
.unwrap()
.focused = Some(true);
}
baseview::WindowEvent::Unfocused => {
let mut egui_input = self.egui_input.borrow_mut();
egui_input.events.push(egui::Event::WindowFocused(false));
egui_input
.viewports
.get_mut(&self.viewport_id)
.unwrap()
.focused = Some(false);
}
baseview::WindowEvent::WillClose => {}
_ => {}
},
_ => {}
}
match &event {
baseview::Event::Keyboard(_) => {
let egui_ctx = &self.inner.borrow().egui_ctx;
if return_status == EventStatus::Captured && !egui_ctx.egui_wants_keyboard_input() {
EventStatus::Ignored
} else {
return_status
}
}
baseview::Event::Mouse(_) => {
let egui_ctx = &self.inner.borrow().egui_ctx;
if egui_ctx.egui_is_using_pointer() || egui_ctx.egui_wants_pointer_input() {
EventStatus::Captured
} else {
EventStatus::Ignored
}
}
baseview::Event::Window(_) => EventStatus::Captured,
_ => EventStatus::Ignored,
}
}
}
fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
(modifiers.command && keycode == keyboard_types::Code::KeyX)
|| (cfg!(target_os = "windows")
&& modifiers.shift
&& keycode == keyboard_types::Code::Delete)
}
fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
(modifiers.command && keycode == keyboard_types::Code::KeyC)
|| (cfg!(target_os = "windows")
&& modifiers.ctrl
&& keycode == keyboard_types::Code::Insert)
}
fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
(modifiers.command && keycode == keyboard_types::Code::KeyV)
|| (cfg!(target_os = "windows")
&& modifiers.shift
&& keycode == keyboard_types::Code::Insert)
}
fn logical_screen_rect(size: WindowSize) -> Rect {
Rect::from_min_size(
Pos2::new(0f32, 0f32),
vec2(size.logical.width as f32, size.logical.height as f32),
)
}