egui_miniquad/
lib.rs

1//! [egui](https://github.com/emilk/egui) bindings for [miniquad](https://github.com/not-fl3/miniquad).
2//!
3//! ## Usage
4//! Create an instance of [`EguiMq`] and call its event-handler from
5//! your `miniquad::EventHandler` implementation.
6//!
7//! In your `miniquad::EventHandler::draw` method do this:
8//!
9//! ```
10//! use miniquad as mq;
11//!
12//! struct MyMiniquadApp {
13//!     egui_mq: egui_miniquad::EguiMq,
14//!     mq_ctx: Box<dyn mq::RenderingBackend>
15//! }
16//!
17//! impl MyMiniquadApp {
18//!     fn new() -> Self {
19//!        let mut mq_ctx = mq::window::new_rendering_backend();
20//!         Self {
21//!             egui_mq: egui_miniquad::EguiMq::new(&mut *mq_ctx),
22//!             mq_ctx,
23//!         }
24//!     }
25//! }
26//!
27//! impl mq::EventHandler for MyMiniquadApp {
28//!     fn update(&mut self) {}
29//!
30//!     fn draw(&mut self) {
31//!         self.mq_ctx.begin_default_pass(mq::PassAction::clear_color(0.0, 0.0, 0.0, 1.0));
32//!         self.mq_ctx.end_render_pass();
33//!
34//!         self.egui_mq.run(&mut *self.mq_ctx, |_mq_ctx, egui_ctx|{
35//!             egui::Window::new("Egui Window").show(egui_ctx, |ui| {
36//!                 ui.heading("Hello World!");
37//!             });
38//!         });
39//!
40//!         // Draw things behind egui here
41//!
42//!         self.egui_mq.draw(&mut *self.mq_ctx);
43//!
44//!         // Draw things in front of egui here
45//!
46//!         self.mq_ctx.commit_frame();
47//!     }
48//!
49//!     fn mouse_motion_event(&mut self, x: f32, y: f32) {
50//!         self.egui_mq.mouse_motion_event(x, y);
51//!     }
52//!
53//!     fn mouse_wheel_event(&mut self, dx: f32, dy: f32) {
54//!         self.egui_mq.mouse_wheel_event(dx, dy);
55//!     }
56//!
57//!     fn mouse_button_down_event(
58//!         &mut self,
59//!         mb: mq::MouseButton,
60//!         x: f32,
61//!         y: f32,
62//!     ) {
63//!         self.egui_mq.mouse_button_down_event(mb, x, y);
64//!     }
65//!
66//!     fn mouse_button_up_event(
67//!         &mut self,
68//!         mb: mq::MouseButton,
69//!         x: f32,
70//!         y: f32,
71//!     ) {
72//!         self.egui_mq.mouse_button_up_event(mb, x, y);
73//!     }
74//!
75//!     fn char_event(
76//!         &mut self,
77//!         character: char,
78//!         _keymods: mq::KeyMods,
79//!         _repeat: bool,
80//!     ) {
81//!         self.egui_mq.char_event(character);
82//!     }
83//!
84//!     fn key_down_event(
85//!         &mut self,
86//!         keycode: mq::KeyCode,
87//!         keymods: mq::KeyMods,
88//!         _repeat: bool,
89//!     ) {
90//!         self.egui_mq.key_down_event(keycode, keymods);
91//!     }
92//!
93//!     fn key_up_event(&mut self, keycode: mq::KeyCode, keymods: mq::KeyMods) {
94//!         self.egui_mq.key_up_event(keycode, keymods);
95//!     }
96//! }
97//! ```
98
99mod input;
100mod painter;
101
102// ----------------------------------------------------------------------------
103
104/// Required by `getrandom` crate.
105#[cfg(target_arch = "wasm32")]
106fn getrandom(buf: &mut [u8]) -> Result<(), getrandom::Error> {
107    // TODO: higher quality random function, e.g. by defining this in JavaScript
108    for value in buf {
109        *value = quad_rand::rand() as u8;
110    }
111    Ok(())
112}
113#[cfg(target_arch = "wasm32")]
114getrandom::register_custom_getrandom!(getrandom);
115
116// ----------------------------------------------------------------------------
117
118use egui::CursorIcon;
119use miniquad as mq;
120
121pub use painter::CallbackFn;
122
123#[cfg(target_os = "macos")] // https://github.com/not-fl3/miniquad/issues/172
124use copypasta::ClipboardProvider;
125
126/// egui bindings for miniquad.
127///
128///
129pub struct EguiMq {
130    /// The DPI as reported by miniquad.
131    native_dpi_scale: f32,
132    /// Pixels per point from egui. Can differ from native DPI because egui allows zooming.
133    pixels_per_point: f32,
134    egui_ctx: egui::Context,
135    egui_input: egui::RawInput,
136    painter: painter::Painter,
137    #[cfg(target_os = "macos")]
138    clipboard: Option<copypasta::ClipboardContext>,
139    shapes: Option<Vec<egui::epaint::ClippedShape>>,
140    textures_delta: egui::TexturesDelta,
141}
142
143impl EguiMq {
144    pub fn new(mq_ctx: &mut dyn mq::RenderingBackend) -> Self {
145        let native_dpi_scale = miniquad::window::dpi_scale();
146
147        Self {
148            native_dpi_scale,
149            pixels_per_point: native_dpi_scale,
150            egui_ctx: egui::Context::default(),
151            painter: painter::Painter::new(mq_ctx),
152            egui_input: egui::RawInput::default(),
153            #[cfg(target_os = "macos")]
154            clipboard: init_clipboard(),
155            shapes: None,
156            textures_delta: Default::default(),
157        }
158    }
159
160    /// Use this to open egui windows, panels etc.
161    ///
162    /// May only be used from inside the callback given to [`Self::run`].
163    pub fn egui_ctx(&self) -> &egui::Context {
164        &self.egui_ctx
165    }
166
167    /// Run the ui code for one frame.
168    pub fn run(
169        &mut self,
170        mq_ctx: &mut dyn mq::RenderingBackend,
171        mut run_ui: impl FnMut(&mut dyn mq::RenderingBackend, &egui::Context),
172    ) {
173        input::on_frame_start(&mut self.egui_input, &self.egui_ctx);
174
175        if self.native_dpi_scale != miniquad::window::dpi_scale() {
176            // DPI scale change (maybe new monitor?). Tell egui to change:
177            self.native_dpi_scale = miniquad::window::dpi_scale();
178            self.egui_input
179                .viewports
180                .get_mut(&self.egui_input.viewport_id)
181                .unwrap()
182                .native_pixels_per_point = Some(self.native_dpi_scale);
183        }
184
185        let full_output = self
186            .egui_ctx
187            .run(self.egui_input.take(), |egui_ctx| run_ui(mq_ctx, egui_ctx));
188
189        let egui::FullOutput {
190            platform_output,
191            textures_delta,
192            shapes,
193            pixels_per_point,
194            viewport_output: _viewport_output, // we only support one viewport
195        } = full_output;
196
197        if self.shapes.is_some() {
198            eprintln!("Egui contents not drawn. You need to call `draw` after calling `run`");
199        }
200        self.shapes = Some(shapes);
201        self.pixels_per_point = pixels_per_point;
202        self.textures_delta.append(textures_delta);
203
204        let egui::PlatformOutput {
205            commands,
206            cursor_icon,
207            events: _,                    // no screen reader
208            ime: _,                       // no IME
209            mutable_text_under_cursor: _, // no IME
210            ..
211        } = platform_output;
212
213        for command in commands {
214            match command {
215                egui::OutputCommand::OpenUrl(open_url) => {
216                    quad_url::link_open(&open_url.url, open_url.new_tab);
217                }
218                egui::OutputCommand::CopyText(copied_text) => {
219                    self.set_clipboard(copied_text);
220                }
221                egui::OutputCommand::CopyImage(_) => (), // No implementation for miniquad
222            }
223        }
224
225        if cursor_icon == egui::CursorIcon::None {
226            miniquad::window::show_mouse(false);
227        } else {
228            miniquad::window::show_mouse(true);
229            let mq_cursor_icon = to_mq_cursor_icon(cursor_icon);
230            let mq_cursor_icon = mq_cursor_icon.unwrap_or(mq::CursorIcon::Default);
231            miniquad::window::set_mouse_cursor(mq_cursor_icon);
232        }
233    }
234
235    /// Call this when you need to draw egui.
236    /// Must be called after `end_frame`.
237    pub fn draw(&mut self, mq_ctx: &mut dyn mq::RenderingBackend) {
238        if let Some(shapes) = self.shapes.take() {
239            let meshes = self.egui_ctx.tessellate(shapes, self.pixels_per_point);
240            self.painter.paint_and_update_textures(
241                mq_ctx,
242                meshes,
243                &self.textures_delta,
244                &self.egui_ctx,
245            );
246            self.textures_delta.clear();
247        } else {
248            eprintln!("Failed to draw egui. You need to call `end_frame` before calling `draw`");
249        }
250    }
251
252    /// Call from your [`miniquad::EventHandler`].
253    pub fn mouse_motion_event(&mut self, x: f32, y: f32) {
254        let pos = egui::pos2(
255            x / self.egui_ctx.pixels_per_point(),
256            y / self.egui_ctx.pixels_per_point(),
257        );
258        self.egui_input.events.push(egui::Event::PointerMoved(pos))
259    }
260
261    /// Call from your [`miniquad::EventHandler`].
262    pub fn mouse_wheel_event(&mut self, dx: f32, dy: f32) {
263        let delta = egui::vec2(dx, dy);
264        let modifiers = self.egui_input.modifiers;
265
266        self.egui_input.events.push(egui::Event::MouseWheel {
267            modifiers,
268            unit: egui::MouseWheelUnit::Line,
269            delta,
270        });
271    }
272
273    /// Call from your [`miniquad::EventHandler`].
274    pub fn mouse_button_down_event(&mut self, mb: mq::MouseButton, x: f32, y: f32) {
275        let pos = egui::pos2(
276            x / self.egui_ctx.pixels_per_point(),
277            y / self.egui_ctx.pixels_per_point(),
278        );
279        let button = to_egui_button(mb);
280        self.egui_input.events.push(egui::Event::PointerButton {
281            pos,
282            button,
283            pressed: true,
284            modifiers: self.egui_input.modifiers,
285        })
286    }
287
288    /// Call from your [`miniquad::EventHandler`].
289    pub fn mouse_button_up_event(&mut self, mb: mq::MouseButton, x: f32, y: f32) {
290        let pos = egui::pos2(
291            x / self.egui_ctx.pixels_per_point(),
292            y / self.egui_ctx.pixels_per_point(),
293        );
294        let button = to_egui_button(mb);
295
296        self.egui_input.events.push(egui::Event::PointerButton {
297            pos,
298            button,
299            pressed: false,
300            modifiers: self.egui_input.modifiers,
301        })
302    }
303
304    /// Call from your [`miniquad::EventHandler`].
305    pub fn char_event(&mut self, chr: char) {
306        if input::is_printable_char(chr)
307            && !self.egui_input.modifiers.ctrl
308            && !self.egui_input.modifiers.mac_cmd
309        {
310            self.egui_input
311                .events
312                .push(egui::Event::Text(chr.to_string()));
313        }
314    }
315
316    /// Call from your [`miniquad::EventHandler`].
317    pub fn key_down_event(&mut self, keycode: mq::KeyCode, keymods: mq::KeyMods) {
318        let modifiers = input::egui_modifiers_from_mq_modifiers(keymods);
319        self.egui_input.modifiers = modifiers;
320
321        if modifiers.command && keycode == mq::KeyCode::X {
322            self.egui_input.events.push(egui::Event::Cut);
323        } else if modifiers.command && keycode == mq::KeyCode::C {
324            self.egui_input.events.push(egui::Event::Copy);
325        } else if modifiers.command && keycode == mq::KeyCode::V {
326            if let Some(text) = self.get_clipboard() {
327                self.egui_input.events.push(egui::Event::Text(text));
328            }
329        } else if let Some(key) = input::egui_key_from_mq_key(keycode) {
330            self.egui_input.events.push(egui::Event::Key {
331                key,
332                pressed: true,
333                modifiers,
334                repeat: false,      // egui will set this for us
335                physical_key: None, // unsupported
336            })
337        }
338    }
339
340    /// Call from your [`miniquad::EventHandler`].
341    pub fn key_up_event(&mut self, keycode: mq::KeyCode, keymods: mq::KeyMods) {
342        let modifiers = input::egui_modifiers_from_mq_modifiers(keymods);
343        self.egui_input.modifiers = modifiers;
344        if let Some(key) = input::egui_key_from_mq_key(keycode) {
345            self.egui_input.events.push(egui::Event::Key {
346                key,
347                pressed: false,
348                modifiers,
349                repeat: false,      // egui will set this for us
350                physical_key: None, // unsupported
351            })
352        }
353    }
354
355    #[cfg(not(target_os = "macos"))]
356    fn set_clipboard(&mut self, text: String) {
357        mq::window::clipboard_set(&text);
358    }
359
360    #[cfg(not(target_os = "macos"))]
361    fn get_clipboard(&mut self) -> Option<String> {
362        mq::window::clipboard_get()
363    }
364
365    #[cfg(target_os = "macos")]
366    fn set_clipboard(&mut self, text: String) {
367        if let Some(clipboard) = &mut self.clipboard {
368            if let Err(err) = clipboard.set_contents(text) {
369                eprintln!("Copy/Cut error: {}", err);
370            }
371        }
372    }
373
374    #[cfg(target_os = "macos")]
375    fn get_clipboard(&mut self) -> Option<String> {
376        if let Some(clipboard) = &mut self.clipboard {
377            match clipboard.get_contents() {
378                Ok(contents) => Some(contents),
379                Err(err) => {
380                    eprintln!("Paste error: {}", err);
381                    None
382                }
383            }
384        } else {
385            None
386        }
387    }
388}
389
390#[cfg(target_os = "macos")]
391fn init_clipboard() -> Option<copypasta::ClipboardContext> {
392    match copypasta::ClipboardContext::new() {
393        Ok(clipboard) => Some(clipboard),
394        Err(err) => {
395            eprintln!("Failed to initialize clipboard: {}", err);
396            None
397        }
398    }
399}
400
401fn to_egui_button(mb: mq::MouseButton) -> egui::PointerButton {
402    match mb {
403        mq::MouseButton::Left => egui::PointerButton::Primary,
404        mq::MouseButton::Right => egui::PointerButton::Secondary,
405        mq::MouseButton::Middle => egui::PointerButton::Middle,
406        mq::MouseButton::Unknown => egui::PointerButton::Primary,
407    }
408}
409
410fn to_mq_cursor_icon(cursor_icon: egui::CursorIcon) -> Option<mq::CursorIcon> {
411    match cursor_icon {
412        // Handled outside this function
413        CursorIcon::None => None,
414
415        egui::CursorIcon::Default => Some(mq::CursorIcon::Default),
416        egui::CursorIcon::PointingHand => Some(mq::CursorIcon::Pointer),
417        egui::CursorIcon::Text => Some(mq::CursorIcon::Text),
418        egui::CursorIcon::ResizeHorizontal => Some(mq::CursorIcon::EWResize),
419        egui::CursorIcon::ResizeVertical => Some(mq::CursorIcon::NSResize),
420        egui::CursorIcon::ResizeNeSw => Some(mq::CursorIcon::NESWResize),
421        egui::CursorIcon::ResizeNwSe => Some(mq::CursorIcon::NWSEResize),
422        egui::CursorIcon::Help => Some(mq::CursorIcon::Help),
423        egui::CursorIcon::Wait => Some(mq::CursorIcon::Wait),
424        egui::CursorIcon::Crosshair => Some(mq::CursorIcon::Crosshair),
425        egui::CursorIcon::Move => Some(mq::CursorIcon::Move),
426        egui::CursorIcon::NotAllowed => Some(mq::CursorIcon::NotAllowed),
427
428        // Similar enough
429        egui::CursorIcon::AllScroll => Some(mq::CursorIcon::Move),
430        egui::CursorIcon::Progress => Some(mq::CursorIcon::Wait),
431
432        // Not implemented, see https://github.com/not-fl3/miniquad/pull/173 and https://github.com/not-fl3/miniquad/issues/171
433        egui::CursorIcon::Grab | egui::CursorIcon::Grabbing => None,
434
435        // Also not implemented:
436        egui::CursorIcon::Alias
437        | egui::CursorIcon::Cell
438        | egui::CursorIcon::ContextMenu
439        | egui::CursorIcon::Copy
440        | egui::CursorIcon::NoDrop
441        | egui::CursorIcon::ResizeColumn
442        | egui::CursorIcon::ResizeEast
443        | egui::CursorIcon::ResizeNorth
444        | egui::CursorIcon::ResizeNorthEast
445        | egui::CursorIcon::ResizeNorthWest
446        | egui::CursorIcon::ResizeRow
447        | egui::CursorIcon::ResizeSouth
448        | egui::CursorIcon::ResizeSouthEast
449        | egui::CursorIcon::ResizeSouthWest
450        | egui::CursorIcon::ResizeWest
451        | egui::CursorIcon::VerticalText
452        | egui::CursorIcon::ZoomIn
453        | egui::CursorIcon::ZoomOut => None,
454    }
455}