119 | where
120 | B: FnMut(&egui::Context, &mut Queue, &mut State),
121 | B: 'static + Send,
122 | {
123 | let renderer = Renderer::new(window, graphics_config).unwrap_or_else(|err| {
124 | // TODO: better error log and not panicking, but that's gonna require baseview changes
125 | log::error!("oops! the gpu backend couldn't initialize! \n {err}");
126 | panic!("gpu backend failed to initialize: \n {err}")
127 | });
128 | let egui_ctx = egui::Context::default();
129 |
130 | // Assume scale for now until there is an event with a new one.
131 | let pixels_per_point = match open_settings.scale_policy {
132 | WindowScalePolicy::ScaleFactor(scale) => scale,
133 | WindowScalePolicy::SystemScaleFactor => 1.0,
134 | } as f32;
135 | let points_per_pixel = pixels_per_point.recip();
136 |
137 | let screen_rect = Rect::from_min_size(
138 | Pos2::new(0f32, 0f32),
139 | vec2(
140 | open_settings.logical_width as f32,
141 | open_settings.logical_height as f32,
142 | ),
143 | );
144 |
145 | let viewport_info = egui::ViewportInfo {
146 | parent: None,
147 | title: Some(open_settings.title),
148 | native_pixels_per_point: Some(pixels_per_point),
149 | focused: Some(true),
150 | inner_rect: Some(screen_rect),
151 | ..Default::default()
152 | };
153 | let viewport_id = egui::ViewportId::default();
154 |
155 | let mut egui_input = egui::RawInput {
156 | max_texture_side: Some(renderer.max_texture_side()),
157 | screen_rect: Some(screen_rect),
158 | ..Default::default()
159 | };
160 | let _ = egui_input.viewports.insert(viewport_id, viewport_info);
161 |
162 | let mut physical_size = PhySize {
163 | width: (open_settings.logical_width * pixels_per_point as f64).round() as u32,
164 | height: (open_settings.logical_height * pixels_per_point as f64).round() as u32,
165 | };
166 |
167 | let mut bg_color = Rgba::BLACK;
168 | let mut close_requested = false;
169 | let mut queue = Queue::new(&mut bg_color, &mut close_requested, &mut physical_size);
170 | (build)(&egui_ctx, &mut queue, &mut state);
171 |
172 | let clipboard_ctx = match copypasta::ClipboardContext::new() {
173 | Ok(clipboard_ctx) => Some(clipboard_ctx),
174 | Err(e) => {
175 | log::error!("Failed to initialize clipboard: {}", e);
176 | None
177 | }
178 | };
179 |
180 | let start_time = Instant::now();
181 |
182 | Self {
183 | user_state: Some(state),
184 | user_update: update,
185 |
186 | egui_ctx,
187 | viewport_id,
188 | start_time,
189 | egui_input,
190 | pointer_pos_in_points: None,
191 | current_cursor_icon: baseview::MouseCursor::Default,
192 |
193 | renderer,
194 |
195 | clipboard_ctx,
196 |
197 | physical_size,
198 | pixels_per_point,
199 | points_per_pixel,
200 | scale_policy: open_settings.scale_policy,
201 | bg_color,
202 | close_requested,
203 | repaint_after: Some(start_time),
204 | }
205 | }
206 |
207 | /// Open a new child window.
208 | ///
209 | /// * `parent` - The parent window.
210 | /// * `settings` - The settings of the window.
211 | /// * `state` - The initial state of your application.
212 | /// * `build` - Called once before the first frame. Allows you to do setup code and to
213 | /// call `ctx.set_fonts()`. Optional.
214 | /// * `update` - Called before each frame. Here you should update the state of your
215 | /// application and build the UI.
216 | pub fn open_parented(
217 | parent: &P,
218 | #[allow(unused_mut)] mut settings: WindowOpenOptions,
219 | graphics_config: GraphicsConfig,
220 | state: State,
221 | build: B,
222 | update: U,
223 | ) -> WindowHandle
224 | where
225 | P: HasRawWindowHandle,
226 | B: FnMut(&egui::Context, &mut Queue, &mut State),
227 | B: 'static + Send,
228 | {
229 | #[cfg(feature = "opengl")]
230 | if settings.gl_config.is_none() {
231 | settings.gl_config = Some(Default::default());
232 | }
233 |
234 | let open_settings = OpenSettings::new(&settings);
235 |
236 | Window::open_parented(
237 | parent,
238 | settings,
239 | move |window: &mut baseview::Window<'_>| -> EguiWindow {
240 | EguiWindow::new(window, open_settings, graphics_config, build, update, state)
241 | },
242 | )
243 | }
244 |
245 | /// Open a new window that blocks the current thread until the window is destroyed.
246 | ///
247 | /// * `settings` - The settings of the window.
248 | /// * `state` - The initial state of your application.
249 | /// * `build` - Called once before the first frame. Allows you to do setup code and to
250 | /// call `ctx.set_fonts()`. Optional.
251 | /// * `update` - Called before each frame. Here you should update the state of your
252 | /// application and build the UI.
253 | pub fn open_blocking(
254 | #[allow(unused_mut)] mut settings: WindowOpenOptions,
255 | graphics_config: GraphicsConfig,
256 | state: State,
257 | build: B,
258 | update: U,
259 | ) where
260 | B: FnMut(&egui::Context, &mut Queue, &mut State),
261 | B: 'static + Send,
262 | {
263 | #[cfg(feature = "opengl")]
264 | if settings.gl_config.is_none() {
265 | settings.gl_config = Some(Default::default());
266 | }
267 |
268 | let open_settings = OpenSettings::new(&settings);
269 |
270 | Window::open_blocking(
271 | settings,
272 | move |window: &mut baseview::Window<'_>| -> EguiWindow {
273 | EguiWindow::new(window, open_settings, graphics_config, build, update, state)
274 | },
275 | )
276 | }
277 |
278 | /// Update the pressed key modifiers when a mouse event has sent a new set of modifiers.
279 | fn update_modifiers(&mut self, modifiers: &Modifiers) {
280 | self.egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty();
281 | self.egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty();
282 | self.egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty();
283 | }
284 | }
285 |
286 | impl WindowHandler for EguiWindow
287 | where
288 | State: 'static + Send,
289 | U: FnMut(&egui::Context, &mut Queue, &mut State),
290 | U: 'static + Send,
291 | {
292 | fn on_frame(&mut self, window: &mut Window) {
293 | let Some(state) = &mut self.user_state else {
294 | return;
295 | };
296 |
297 | self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
298 | self.egui_input.screen_rect = Some(calculate_screen_rect(
299 | self.physical_size,
300 | self.points_per_pixel,
301 | ));
302 |
303 | self.egui_ctx.begin_pass(self.egui_input.take());
304 |
305 | //let mut repaint_requested = false;
306 | let mut queue = Queue::new(
307 | &mut self.bg_color,
308 | &mut self.close_requested,
309 | &mut self.physical_size,
310 | );
311 |
312 | (self.user_update)(&self.egui_ctx, &mut queue, state);
313 |
314 | if self.close_requested {
315 | window.close();
316 | }
317 |
318 | // Prevent data from being allocated every frame by storing this
319 | // in a member field.
320 | let mut full_output = self.egui_ctx.end_pass();
321 |
322 | let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
323 | // The main window was closed by egui.
324 | window.close();
325 | return;
326 | };
327 |
328 | for command in viewport_output.commands.iter() {
329 | match command {
330 | ViewportCommand::Close => {
331 | window.close();
332 | }
333 | ViewportCommand::InnerSize(size) => window.resize(baseview::Size {
334 | width: size.x.max(1.0) as f64,
335 | height: size.y.max(1.0) as f64,
336 | }),
337 | _ => {}
338 | }
339 | }
340 |
341 | let now = Instant::now();
342 | let do_repaint_now = if let Some(t) = self.repaint_after {
343 | now >= t || viewport_output.repaint_delay.is_zero()
344 | } else {
345 | viewport_output.repaint_delay.is_zero()
346 | };
347 |
348 | if do_repaint_now {
349 | self.renderer.render(
350 | #[cfg(feature = "opengl")]
351 | window,
352 | self.bg_color,
353 | self.physical_size,
354 | self.pixels_per_point,
355 | &mut self.egui_ctx,
356 | &mut full_output,
357 | );
358 |
359 | self.repaint_after = None;
360 | } else if let Some(repaint_after) = now.checked_add(viewport_output.repaint_delay) {
361 | // Schedule to repaint after the requested time has elapsed.
362 | self.repaint_after = Some(repaint_after);
363 | }
364 |
365 | for command in full_output.platform_output.commands {
366 | match command {
367 | egui::OutputCommand::CopyText(text) => {
368 | if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
369 | if let Err(err) = clipboard_ctx.set_contents(text) {
370 | log::error!("Copy/Cut error: {}", err);
371 | }
372 | }
373 | }
374 | egui::OutputCommand::CopyImage(_) => {
375 | log::warn!("Copying images is not supported in egui_baseview.");
376 | }
377 | egui::OutputCommand::OpenUrl(open_url) => {
378 | if let Err(err) = open::that_detached(&open_url.url) {
379 | log::error!("Open error: {}", err);
380 | }
381 | }
382 | }
383 | }
384 |
385 | let cursor_icon =
386 | crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
387 | if self.current_cursor_icon != cursor_icon {
388 | self.current_cursor_icon = cursor_icon;
389 |
390 | // TODO: Set mouse cursor for MacOS once baseview supports it.
391 | #[cfg(not(target_os = "macos"))]
392 | window.set_mouse_cursor(cursor_icon);
393 | }
394 |
395 | // A temporary workaround for keyboard input not working sometimes in Windows.
396 | // See https://github.com/BillyDM/egui-baseview/issues/20
397 | #[cfg(feature = "windows_keyboard_workaround")]
398 | {
399 | #[cfg(target_os = "windows")]
400 | {
401 | if !full_output.platform_output.events.is_empty()
402 | || full_output.platform_output.ime.is_some()
403 | {
404 | window.focus();
405 | }
406 | }
407 | }
408 | }
409 |
410 | fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus {
411 | match &event {
412 | baseview::Event::Mouse(event) => match event {
413 | baseview::MouseEvent::CursorMoved {
414 | position,
415 | modifiers,
416 | } => {
417 | self.update_modifiers(modifiers);
418 |
419 | let pos = pos2(position.x as f32, position.y as f32);
420 | self.pointer_pos_in_points = Some(pos);
421 | self.egui_input.events.push(egui::Event::PointerMoved(pos));
422 | }
423 | baseview::MouseEvent::ButtonPressed { button, modifiers } => {
424 | self.update_modifiers(modifiers);
425 |
426 | if let Some(pos) = self.pointer_pos_in_points {
427 | if let Some(button) = crate::translate::translate_mouse_button(*button) {
428 | self.egui_input.events.push(egui::Event::PointerButton {
429 | pos,
430 | button,
431 | pressed: true,
432 | modifiers: self.egui_input.modifiers,
433 | });
434 | }
435 | }
436 | }
437 | baseview::MouseEvent::ButtonReleased { button, modifiers } => {
438 | self.update_modifiers(modifiers);
439 |
440 | if let Some(pos) = self.pointer_pos_in_points {
441 | if let Some(button) = crate::translate::translate_mouse_button(*button) {
442 | self.egui_input.events.push(egui::Event::PointerButton {
443 | pos,
444 | button,
445 | pressed: false,
446 | modifiers: self.egui_input.modifiers,
447 | });
448 | }
449 | }
450 | }
451 | baseview::MouseEvent::WheelScrolled {
452 | delta: scroll_delta,
453 | modifiers,
454 | } => {
455 | self.update_modifiers(modifiers);
456 |
457 | #[allow(unused_mut)]
458 | let (unit, mut delta) = match scroll_delta {
459 | baseview::ScrollDelta::Lines { x, y } => {
460 | (egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
461 | }
462 |
463 | baseview::ScrollDelta::Pixels { x, y } => (
464 | egui::MouseWheelUnit::Point,
465 | egui::vec2(*x, *y) * self.points_per_pixel,
466 | ),
467 | };
468 |
469 | if cfg!(target_os = "macos") {
470 | // This is still buggy in winit despite
471 | // https://github.com/rust-windowing/winit/issues/1695 being closed
472 | //
473 | // TODO: See if this is an issue in baseview as well.
474 | delta.x *= -1.0;
475 | }
476 |
477 | self.egui_input.events.push(egui::Event::MouseWheel {
478 | unit,
479 | delta,
480 | modifiers: self.egui_input.modifiers,
481 | });
482 | }
483 | baseview::MouseEvent::CursorLeft => {
484 | self.pointer_pos_in_points = None;
485 | self.egui_input.events.push(egui::Event::PointerGone);
486 | }
487 | _ => {}
488 | },
489 | baseview::Event::Keyboard(event) => {
490 | use keyboard_types::Code;
491 |
492 | let pressed = event.state == keyboard_types::KeyState::Down;
493 |
494 | match event.code {
495 | Code::ShiftLeft | Code::ShiftRight => self.egui_input.modifiers.shift = pressed,
496 | Code::ControlLeft | Code::ControlRight => {
497 | self.egui_input.modifiers.ctrl = pressed;
498 |
499 | #[cfg(not(target_os = "macos"))]
500 | {
501 | self.egui_input.modifiers.command = pressed;
502 | }
503 | }
504 | Code::AltLeft | Code::AltRight => self.egui_input.modifiers.alt = pressed,
505 | Code::MetaLeft | Code::MetaRight => {
506 | #[cfg(target_os = "macos")]
507 | {
508 | self.egui_input.modifiers.mac_cmd = pressed;
509 | self.egui_input.modifiers.command = pressed;
510 | }
511 | // prevent `rustfmt` from breaking this
512 | }
513 | _ => (),
514 | }
515 |
516 | if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
517 | self.egui_input.events.push(egui::Event::Key {
518 | key,
519 | physical_key: None,
520 | pressed,
521 | repeat: event.repeat,
522 | modifiers: self.egui_input.modifiers,
523 | });
524 | }
525 |
526 | if pressed {
527 | // VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
528 | // so we detect these things manually:
529 | //
530 | // TODO: See if this is an issue in baseview as well.
531 | if is_cut_command(self.egui_input.modifiers, event.code) {
532 | self.egui_input.events.push(egui::Event::Cut);
533 | } else if is_copy_command(self.egui_input.modifiers, event.code) {
534 | self.egui_input.events.push(egui::Event::Copy);
535 | } else if is_paste_command(self.egui_input.modifiers, event.code) {
536 | if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
537 | match clipboard_ctx.get_contents() {
538 | Ok(contents) => {
539 | self.egui_input.events.push(egui::Event::Text(contents))
540 | }
541 | Err(err) => {
542 | log::error!("Paste error: {}", err);
543 | }
544 | }
545 | }
546 | } else if let keyboard_types::Key::Character(written) = &event.key {
547 | if !self.egui_input.modifiers.ctrl && !self.egui_input.modifiers.command {
548 | self.egui_input
549 | .events
550 | .push(egui::Event::Text(written.clone()));
551 | }
552 | }
553 | }
554 | }
555 | baseview::Event::Window(event) => match event {
556 | baseview::WindowEvent::Resized(window_info) => {
557 | self.pixels_per_point = match self.scale_policy {
558 | WindowScalePolicy::ScaleFactor(scale) => scale,
559 | WindowScalePolicy::SystemScaleFactor => window_info.scale(),
560 | } as f32;
561 | self.points_per_pixel = self.pixels_per_point.recip();
562 |
563 | self.physical_size = window_info.physical_size();
564 |
565 | let screen_rect =
566 | calculate_screen_rect(self.physical_size, self.points_per_pixel);
567 |
568 | self.egui_input.screen_rect = Some(screen_rect);
569 |
570 | let viewport_info = self
571 | .egui_input
572 | .viewports
573 | .get_mut(&self.viewport_id)
574 | .unwrap();
575 | viewport_info.native_pixels_per_point = Some(self.pixels_per_point);
576 | viewport_info.inner_rect = Some(screen_rect);
577 |
578 | // Schedule to repaint on the next frame.
579 | self.repaint_after = Some(Instant::now());
580 | }
581 | baseview::WindowEvent::Focused => {
582 | self.egui_input
583 | .events
584 | .push(egui::Event::WindowFocused(true));
585 | self.egui_input
586 | .viewports
587 | .get_mut(&self.viewport_id)
588 | .unwrap()
589 | .focused = Some(true);
590 | }
591 | baseview::WindowEvent::Unfocused => {
592 | self.egui_input
593 | .events
594 | .push(egui::Event::WindowFocused(false));
595 | self.egui_input
596 | .viewports
597 | .get_mut(&self.viewport_id)
598 | .unwrap()
599 | .focused = Some(false);
600 | }
601 | baseview::WindowEvent::WillClose => {}
602 | },
603 | }
604 |
605 | EventStatus::Captured
606 | }
607 | }
608 |
609 | fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
610 | (modifiers.command && keycode == keyboard_types::Code::KeyX)
611 | || (cfg!(target_os = "windows")
612 | && modifiers.shift
613 | && keycode == keyboard_types::Code::Delete)
614 | }
615 |
616 | fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
617 | (modifiers.command && keycode == keyboard_types::Code::KeyC)
618 | || (cfg!(target_os = "windows")
619 | && modifiers.ctrl
620 | && keycode == keyboard_types::Code::Insert)
621 | }
622 |
623 | fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
624 | (modifiers.command && keycode == keyboard_types::Code::KeyV)
625 | || (cfg!(target_os = "windows")
626 | && modifiers.shift
627 | && keycode == keyboard_types::Code::Insert)
628 | }
629 |
630 | /// Calculate screen rectangle in logical size.
631 | fn calculate_screen_rect(physical_size: PhySize, points_per_pixel: f32) -> Rect {
632 | let logical_size = (
633 | physical_size.width as f32 * points_per_pixel,
634 | physical_size.height as f32 * points_per_pixel,
635 | );
636 | Rect::from_min_size(Pos2::new(0f32, 0f32), vec2(logical_size.0, logical_size.1))
637 | }
638 |
--------------------------------------------------------------------------------