egui_ark/
implementation.rs

1use ark::applet::input;
2use ark::render;
3use ark::render::TextureFormat;
4use ark::ColorRgba8;
5use ark::Vec2;
6use std::collections::HashMap;
7
8/// Bindings to the egui library.
9///
10/// Instantiate one [`EguiArk`] in your module.
11///
12/// ``` no_run
13/// # fn applet() -> ark::applet::Applet { unimplemented!() }
14/// # fn render() -> ark::render::Render { unimplemented!() }
15/// let mut egui = EguiArk::default();
16/// let ctx = egui.begin_frame(&applet());
17/// ui(ctx);
18/// let ctx = egui.end_frame(&render(), &applet());
19///
20/// fn ui(ctx: &mut egui::Context) {
21///     egui::Window::new("My window").show(ctx, |ui| {
22///         ui.label("Hello world!");
23///     });
24/// }
25/// ```
26#[derive(Default)]
27pub struct EguiArk {
28    input_mngr: input::InputManager,
29    egui_input: egui::RawInput,
30    ctx: egui::Context,
31    painter: EguiPainter,
32}
33
34impl EguiArk {
35    /// Access to the egui context.
36    pub fn ctx(&self) -> &egui::Context {
37        &self.ctx
38    }
39
40    /// Call before using egui. This gathers input and prepares for a new frame.
41    pub fn begin_frame(&mut self, applet: &ark::applet::Applet) -> egui::Context {
42        puffin::profile_function!();
43        if applet.window_state().is_some() {
44            self.input_mngr.update(applet);
45            update_input(&mut self.egui_input, applet, &self.input_mngr);
46        }
47        puffin::profile_scope!("egui::begin_frame");
48        self.ctx.begin_frame(self.egui_input.take());
49        self.ctx.clone()
50    }
51
52    /// Call after using egui. This will render the interface.
53    pub fn end_frame(&mut self, render: &render::Render, applet: &ark::applet::Applet) {
54        puffin::profile_function!();
55
56        let full_output = {
57            puffin::profile_scope!("egui::end_frame");
58            self.ctx.end_frame()
59        };
60
61        let platform_output = full_output.platform_output;
62
63        if applet.window_state().is_some() {
64            let paint_jobs = {
65                puffin::profile_scope!("egui::tessellate");
66                self.ctx.tessellate(full_output.shapes)
67            };
68            if !platform_output.copied_text.is_empty() {
69                applet.set_clipboard_string(&platform_output.copied_text);
70            }
71
72            if platform_output.cursor_icon == egui::CursorIcon::None {
73                applet.set_cursor_mode(ark::applet::CursorMode::Hide);
74            } else {
75                applet.set_cursor_mode(ark::applet::CursorMode::None);
76                applet.set_cursor_shape(from_egui_cursor(platform_output.cursor_icon));
77            }
78
79            self.painter
80                .upload_font_textures(render, full_output.textures_delta);
81            self.painter.paint(render, &self.ctx, &paint_jobs);
82        }
83    }
84
85    /// True if egui is currently interested in the pointer (mouse/touch).
86    /// Could be the mouse is hovering over a egui window,
87    /// or the user is dragging an egui widget.
88    /// If false, the mouse is outside of any egui area and so
89    /// you may be interested in what it is doing (e.g. controlling your game).
90    pub fn wants_pointer_input(&self) -> bool {
91        self.ctx.wants_pointer_input()
92    }
93
94    /// If true, egui is currently listening on text input (e.g. typing text in a `TextEdit`).
95    pub fn wants_keyboard_input(&self) -> bool {
96        self.ctx.wants_keyboard_input()
97    }
98
99    pub fn is_mouse_down(&self) -> bool {
100        self.ctx.input().pointer.any_down()
101    }
102
103    /// Save egui state (window positions etc)
104    pub fn persist(&self) -> serde_json::Value {
105        serde_json::to_value(&*self.ctx.memory()).unwrap_or_default()
106    }
107
108    /// Restore egui (window positions etc)
109    pub fn restore(&self, value: serde_json::Value) {
110        match serde_json::from_value(value) {
111            Ok(memory) => {
112                *self.ctx.memory() = memory;
113            }
114            Err(err) => {
115                ark::warn!("Failed to restore egui state: {}", err);
116            }
117        }
118    }
119
120    /// Almost the same as `available_rect()`, however, in *physical* pixels, not *logical* (i.e.
121    /// `available_rect()` scaled by the DPI factor).
122    ///
123    /// How much space is still available after panels has been added.
124    /// This is the "background" area, what egui doesn't cover with panels (but may cover with windows).
125    /// This is also the area to which windows are constrained.
126    pub fn viewport(&self) -> ark::render::Rectangle {
127        let available_rect = self.ctx.available_rect();
128        let pixels_per_point = self.ctx.input().pixels_per_point();
129        ark::render::Rectangle {
130            min_x: available_rect.min.x * pixels_per_point,
131            min_y: available_rect.min.y * pixels_per_point,
132            max_x: available_rect.max.x * pixels_per_point,
133            max_y: available_rect.max.y * pixels_per_point,
134        }
135    }
136}
137
138fn from_egui_cursor(cursor: egui::CursorIcon) -> ark::applet::CursorShape {
139    use ark::applet::CursorShape;
140    match cursor {
141        egui::CursorIcon::None => unreachable!("Should have been handled outside this function"),
142        egui::CursorIcon::Default => CursorShape::Default,
143        egui::CursorIcon::ContextMenu => CursorShape::ContextMenu,
144        egui::CursorIcon::Help => CursorShape::Help,
145        egui::CursorIcon::PointingHand => CursorShape::Hand,
146        egui::CursorIcon::Progress => CursorShape::Progress,
147        egui::CursorIcon::Wait => CursorShape::Wait,
148        egui::CursorIcon::Cell => CursorShape::Cell,
149        egui::CursorIcon::Crosshair => CursorShape::Crosshair,
150        egui::CursorIcon::Text => CursorShape::Text,
151        egui::CursorIcon::VerticalText => CursorShape::VerticalText,
152        egui::CursorIcon::Alias => CursorShape::Alias,
153        egui::CursorIcon::Copy => CursorShape::Copy,
154        egui::CursorIcon::Move => CursorShape::Move,
155        egui::CursorIcon::NoDrop => CursorShape::NoDrop,
156        egui::CursorIcon::NotAllowed => CursorShape::NotAllowed,
157        egui::CursorIcon::Grab => CursorShape::Grab,
158        egui::CursorIcon::Grabbing => CursorShape::Grabbing,
159        egui::CursorIcon::AllScroll => CursorShape::AllScroll,
160        egui::CursorIcon::ResizeHorizontal => CursorShape::EWResize,
161        egui::CursorIcon::ResizeNeSw => CursorShape::NESWResize,
162        egui::CursorIcon::ResizeNwSe => CursorShape::NWSEResize,
163        egui::CursorIcon::ResizeVertical => CursorShape::NSResize,
164        egui::CursorIcon::ZoomIn => CursorShape::ZoomIn,
165        egui::CursorIcon::ZoomOut => CursorShape::ZoomOut,
166        egui::CursorIcon::ResizeEast => CursorShape::EResize,
167        egui::CursorIcon::ResizeSouthEast => CursorShape::SEResize,
168        egui::CursorIcon::ResizeSouth => CursorShape::SResize,
169        egui::CursorIcon::ResizeSouthWest => CursorShape::SWResize,
170        egui::CursorIcon::ResizeWest => CursorShape::WResize,
171        egui::CursorIcon::ResizeNorthWest => CursorShape::NWResize,
172        egui::CursorIcon::ResizeNorth => CursorShape::NResize,
173        egui::CursorIcon::ResizeNorthEast => CursorShape::NEResize,
174        egui::CursorIcon::ResizeColumn => CursorShape::RowResize,
175        egui::CursorIcon::ResizeRow => CursorShape::ColResize,
176    }
177}
178
179// ----------------------------------------------------------------------------
180
181impl serde::Serialize for EguiArk {
182    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183    where
184        S: serde::Serializer,
185    {
186        // (&*self.ctx.memory()).serialize(serializer) // ERR: `KeyMustBeString` :(
187
188        self.persist().serialize(serializer)
189    }
190}
191
192impl<'de> serde::Deserialize<'de> for EguiArk {
193    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
194    where
195        D: serde::Deserializer<'de>,
196    {
197        let egui_ark = Self::default();
198
199        // *egui_ark.ctx.memory() = egui::Memory::deserialize(deserializer)?; // Can't do this due to Flexbuffers bug
200
201        egui_ark.restore(serde_json::Value::deserialize(deserializer)?);
202
203        Ok(egui_ark)
204    }
205}
206
207// ----------------------------------------------------------------------------
208
209#[derive(Default)]
210struct EguiPainter {
211    /// The font textures used by egui
212    egui_textures: HashMap<egui::TextureId, render::Texture>,
213
214    // Reuse buffers to avoid allocations:
215    positions: Vec<Vec2>,
216    colors: Vec<ColorRgba8>,
217    uvs: Vec<Vec2>,
218}
219
220impl EguiPainter {
221    pub fn upload_font_textures(
222        &mut self,
223        render: &render::Render,
224        textures_delta: egui::TexturesDelta,
225    ) {
226        puffin::profile_function!();
227        for (texture_id, image_delta) in textures_delta.set {
228            if let Some(pos) = image_delta.pos {
229                // This is an update. Ignore for now until the API is added.
230                if let Some(texture) = self.egui_textures.get_mut(&texture_id) {
231                    // TODO
232                    let pixels = get_rgba_pixels(&image_delta.image);
233                    texture.update_rectangle(
234                        pos[0] as u32,
235                        pos[1] as u32,
236                        image_delta.image.width() as u32,
237                        image_delta.image.height() as u32,
238                        &pixels,
239                    );
240                }
241            } else {
242                self.egui_textures
243                    .insert(texture_id, as_ark_texture(render, &image_delta));
244            }
245        }
246
247        for free in textures_delta.free {
248            self.egui_textures.remove(&free);
249        }
250    }
251
252    pub fn paint(
253        &mut self,
254        render: &render::Render,
255        egui_ctx: &egui::Context,
256        meshes: &[egui::ClippedPrimitive],
257    ) {
258        puffin::profile_function!();
259        let dpi_factor = egui_ctx.input().pixels_per_point();
260        for egui::ClippedPrimitive {
261            primitive,
262            clip_rect,
263        } in meshes
264        {
265            self.paint_mesh(render, dpi_factor, clip_rect, primitive);
266        }
267    }
268
269    fn paint_mesh(
270        &mut self,
271        render: &render::Render,
272        dpi_factor: f32,
273        clip_rect: &egui::Rect,
274        primitive: &egui::epaint::Primitive,
275    ) {
276        puffin::profile_function!();
277
278        let mesh = match primitive {
279            egui::epaint::Primitive::Mesh(mesh) => mesh,
280            _ => return,
281        };
282
283        if !mesh.is_valid() {
284            ark::error!("egui generated an invalid triangle mesh");
285            return;
286        }
287
288        self.positions.clear();
289        self.colors.clear();
290        self.uvs.clear();
291        for v in &mesh.vertices {
292            self.positions
293                .push(dpi_factor * Vec2::new(v.pos.x, v.pos.y));
294            self.colors.push(ColorRgba8(v.color.to_array()));
295            self.uvs.push(Vec2::new(v.uv.x, v.uv.y));
296        }
297
298        let clip_rect = render::Rectangle {
299            min_x: dpi_factor * clip_rect.min.x,
300            min_y: dpi_factor * clip_rect.min.y,
301            max_x: dpi_factor * clip_rect.max.x,
302            max_y: dpi_factor * clip_rect.max.y,
303        };
304
305        let texture_handle = if let Some(texture) = self.egui_textures.get(&mesh.texture_id) {
306            texture.handle()
307        } else {
308            return;
309        };
310
311        let indices: &[u32] = &mesh.indices;
312        let indices: &[[u32; 3]] = unsafe { transmute_slice(indices) };
313        assert_eq!(mesh.indices.len(), 3 * indices.len());
314
315        puffin::profile_scope!("draw_textured_triangles");
316        render.draw_textured_triangles(
317            &clip_rect,
318            texture_handle,
319            indices,
320            &self.positions,
321            &self.colors,
322            &self.uvs,
323        );
324    }
325}
326
327/// Convert e.g. &[[u32; 3]] to three times as long slice of &[u32]
328#[allow(clippy::integer_division)]
329unsafe fn transmute_slice<Target: Copy, Source: Copy>(source: &[Source]) -> &[Target] {
330    use std::mem::size_of;
331
332    let target_len = source.len() * size_of::<Source>() / size_of::<Target>();
333
334    assert_eq!(
335        target_len * size_of::<Target>(),
336        source.len() * size_of::<Source>(),
337        "Source slice length is not an even multiple of the target"
338    );
339    unsafe { std::slice::from_raw_parts(source.as_ptr().cast::<Target>(), target_len) }
340}
341
342fn get_rgba_pixels(image: &egui::epaint::ImageData) -> Vec<u8> {
343    let mut pixels = Vec::with_capacity(image.width() * image.height() * 4);
344    match image {
345        egui::epaint::ImageData::Font(font) => {
346            for srgba in font.srgba_pixels(1.0) {
347                pixels.push(srgba[0]);
348                pixels.push(srgba[1]);
349                pixels.push(srgba[2]);
350                pixels.push(srgba[3]);
351            }
352        }
353        _ => {
354            pixels.shrink_to_fit();
355        }
356    }
357
358    pixels
359}
360
361fn as_ark_texture(
362    render: &render::Render,
363    image_delta: &egui::epaint::ImageDelta,
364) -> render::Texture {
365    let pixels = get_rgba_pixels(&image_delta.image);
366
367    puffin::profile_scope!("render::Texture::create");
368    render
369        .create_texture()
370        .name("egui")
371        .dimensions(image_delta.image.width(), image_delta.image.height())
372        .format(TextureFormat::R8G8B8A8_UNORM)
373        .data(&pixels)
374        .build()
375        .expect("Failed to create egui texture")
376}
377
378/// Update the input given to egui with input pulled from ark
379fn update_input(
380    egui_input: &mut egui::RawInput,
381    applet: &ark::applet::Applet,
382    input_mngr: &input::InputManager,
383) {
384    puffin::profile_function!();
385    let window_state = if let Some(window_state) = applet.window_state() {
386        window_state
387    } else {
388        return;
389    };
390
391    egui_input.screen_rect = Some(egui::Rect::from_min_size(
392        Default::default(),
393        egui::vec2(window_state.width, window_state.height),
394    ));
395    egui_input.pixels_per_point = Some(window_state.dpi_factor);
396    egui_input.time = Some(applet.real_time_since_start());
397    egui_input.events.clear();
398    egui_input.modifiers = as_egui_modifiers(&input_mngr.state().modifiers);
399
400    for (state, event) in input_mngr.events() {
401        match event {
402            input::Event::Key { key, pressed } => {
403                if let Some(key) = as_egui_key(*key) {
404                    egui_input.events.push(egui::Event::Key {
405                        pressed: *pressed,
406                        key,
407                        modifiers: as_egui_modifiers(&state.modifiers),
408                    });
409                }
410            }
411            input::Event::Char(chr) => {
412                if *chr != '\r' {
413                    egui_input.events.push(egui::Event::Text(chr.to_string()));
414                }
415            }
416
417            input::Event::PointerMove { pos, primary, .. } => {
418                if *primary {
419                    egui_input
420                        .events
421                        .push(egui::Event::PointerMoved(egui::pos2(pos.x, pos.y)));
422                }
423            }
424            input::Event::PointerButton {
425                pressed,
426                button,
427                pos,
428                ..
429            } => {
430                egui_input.events.push(egui::Event::PointerButton {
431                    pos: egui::pos2(pos.x, pos.y),
432                    button: as_egui_button(button),
433                    pressed: *pressed,
434                    modifiers: as_egui_modifiers(&state.modifiers),
435                });
436            }
437            input::Event::PointerDelta { .. } => {}
438            input::Event::Scroll { delta, .. } => {
439                egui_input
440                    .events
441                    .push(egui::Event::Scroll(egui::vec2(delta.x, delta.y)));
442            }
443            input::Event::Command(command) => {
444                match command {
445                    input::Command::Copy => egui_input.events.push(egui::Event::Copy),
446                    input::Command::Cut => egui_input.events.push(egui::Event::Cut),
447                    input::Command::Paste => {
448                        if let Some(s) = applet.clipboard_string() {
449                            egui_input.events.push(egui::Event::Text(s));
450                        }
451                    }
452                    // egui checks for Cmd+Z itself
453                    input::Command::Undo | input::Command::Redo | input::Command::Save |
454                    // egui doesn't care
455                    input::Command::New => {}
456                }
457            }
458            input::Event::Axis { .. }
459            | input::Event::GamepadButton { .. }
460            | input::Event::RawMidi { .. } => {}
461        }
462    }
463}
464
465fn as_egui_key(code: input::Key) -> Option<egui::Key> {
466    match code {
467        input::Key::Down => Some(egui::Key::ArrowDown),
468        input::Key::Left => Some(egui::Key::ArrowLeft),
469        input::Key::Right => Some(egui::Key::ArrowRight),
470        input::Key::Up => Some(egui::Key::ArrowUp),
471
472        input::Key::Escape => Some(egui::Key::Escape),
473        input::Key::Tab => Some(egui::Key::Tab),
474        input::Key::Back => Some(egui::Key::Backspace),
475        input::Key::Return => Some(egui::Key::Enter),
476        input::Key::Space => Some(egui::Key::Space),
477
478        input::Key::Insert => Some(egui::Key::Insert),
479        input::Key::Delete => Some(egui::Key::Delete),
480        input::Key::Home => Some(egui::Key::Home),
481        input::Key::End => Some(egui::Key::End),
482        input::Key::PageUp => Some(egui::Key::PageUp),
483        input::Key::PageDown => Some(egui::Key::PageDown),
484
485        input::Key::Key0 => Some(egui::Key::Num0),
486        input::Key::Key1 => Some(egui::Key::Num1),
487        input::Key::Key2 => Some(egui::Key::Num2),
488        input::Key::Key3 => Some(egui::Key::Num3),
489        input::Key::Key4 => Some(egui::Key::Num4),
490        input::Key::Key5 => Some(egui::Key::Num5),
491        input::Key::Key6 => Some(egui::Key::Num6),
492        input::Key::Key7 => Some(egui::Key::Num7),
493        input::Key::Key8 => Some(egui::Key::Num8),
494        input::Key::Key9 => Some(egui::Key::Num9),
495
496        input::Key::A => Some(egui::Key::A),
497        input::Key::B => Some(egui::Key::B),
498        input::Key::C => Some(egui::Key::C),
499        input::Key::D => Some(egui::Key::D),
500        input::Key::E => Some(egui::Key::E),
501        input::Key::F => Some(egui::Key::F),
502        input::Key::G => Some(egui::Key::G),
503        input::Key::H => Some(egui::Key::H),
504        input::Key::I => Some(egui::Key::I),
505        input::Key::J => Some(egui::Key::J),
506        input::Key::K => Some(egui::Key::K),
507        input::Key::L => Some(egui::Key::L),
508        input::Key::M => Some(egui::Key::M),
509        input::Key::N => Some(egui::Key::N),
510        input::Key::O => Some(egui::Key::O),
511        input::Key::P => Some(egui::Key::P),
512        input::Key::Q => Some(egui::Key::Q),
513        input::Key::R => Some(egui::Key::R),
514        input::Key::S => Some(egui::Key::S),
515        input::Key::T => Some(egui::Key::T),
516        input::Key::U => Some(egui::Key::U),
517        input::Key::V => Some(egui::Key::V),
518        input::Key::W => Some(egui::Key::W),
519        input::Key::X => Some(egui::Key::X),
520        input::Key::Y => Some(egui::Key::Y),
521        input::Key::Z => Some(egui::Key::Z),
522
523        _ => None,
524    }
525}
526
527fn as_egui_modifiers(modifiers: &input::Modifiers) -> egui::Modifiers {
528    egui::Modifiers {
529        alt: modifiers.alt,
530        ctrl: modifiers.ctrl,
531        shift: modifiers.shift,
532        mac_cmd: modifiers.cmd, // close enough
533        command: modifiers.cmd,
534    }
535}
536
537fn as_egui_button(button: &input::PointerButton) -> egui::PointerButton {
538    match button {
539        input::PointerButton::Primary => egui::PointerButton::Primary,
540        input::PointerButton::Secondary => egui::PointerButton::Secondary,
541        input::PointerButton::Middle => egui::PointerButton::Middle,
542    }
543}