egui_sf2g/
lib.rs

1//! egui SFML integration helpers
2//!
3//! Contains various types and functions that helps with integrating egui with SFML.
4
5#![warn(missing_docs)]
6
7mod rendering;
8
9pub use {egui, sf2g};
10use {
11    egui::{
12        Context, CursorIcon, Modifiers, PointerButton, Pos2, RawInput, TextureId, ViewportCommand,
13    },
14    sf2g::{
15        cpp::FBox,
16        graphics::{RenderTarget as _, RenderWindow, Texture},
17        system::{Clock, Vector2, Vector2i},
18        window::{Cursor, CursorType, Event, Key, mouse},
19    },
20    std::collections::HashMap,
21};
22
23fn button_conv(button: mouse::Button) -> Option<PointerButton> {
24    let but = match button {
25        mouse::Button::Left => PointerButton::Primary,
26        mouse::Button::Right => PointerButton::Secondary,
27        mouse::Button::Middle => PointerButton::Middle,
28        _ => return None,
29    };
30    Some(but)
31}
32
33fn key_conv(code: Key) -> Option<egui::Key> {
34    use egui::Key as EKey;
35    Some(match code {
36        Key::Down => EKey::ArrowDown,
37        Key::Left => EKey::ArrowLeft,
38        Key::Right => EKey::ArrowRight,
39        Key::Up => EKey::ArrowUp,
40        Key::Escape => EKey::Escape,
41        Key::Tab => EKey::Tab,
42        Key::Backspace => EKey::Backspace,
43        Key::Enter => EKey::Enter,
44        Key::Space => EKey::Space,
45        Key::Insert => EKey::Insert,
46        Key::Delete => EKey::Delete,
47        Key::Home => EKey::Home,
48        Key::End => EKey::End,
49        Key::PageUp => EKey::PageUp,
50        Key::PageDown => EKey::PageDown,
51        Key::LBracket => EKey::OpenBracket,
52        Key::RBracket => EKey::CloseBracket,
53        Key::Num0 => EKey::Num0,
54        Key::Num1 => EKey::Num1,
55        Key::Num2 => EKey::Num2,
56        Key::Num3 => EKey::Num3,
57        Key::Num4 => EKey::Num4,
58        Key::Num5 => EKey::Num5,
59        Key::Num6 => EKey::Num6,
60        Key::Num7 => EKey::Num7,
61        Key::Num8 => EKey::Num8,
62        Key::Num9 => EKey::Num9,
63        Key::A => EKey::A,
64        Key::B => EKey::B,
65        Key::C => EKey::C,
66        Key::D => EKey::D,
67        Key::E => EKey::E,
68        Key::F => EKey::F,
69        Key::G => EKey::G,
70        Key::H => EKey::H,
71        Key::I => EKey::I,
72        Key::J => EKey::J,
73        Key::K => EKey::K,
74        Key::L => EKey::L,
75        Key::M => EKey::M,
76        Key::N => EKey::N,
77        Key::O => EKey::O,
78        Key::P => EKey::P,
79        Key::Q => EKey::Q,
80        Key::R => EKey::R,
81        Key::S => EKey::S,
82        Key::T => EKey::T,
83        Key::U => EKey::U,
84        Key::V => EKey::V,
85        Key::W => EKey::W,
86        Key::X => EKey::X,
87        Key::Y => EKey::Y,
88        Key::Z => EKey::Z,
89        Key::F1 => EKey::F1,
90        Key::F2 => EKey::F2,
91        Key::F3 => EKey::F3,
92        Key::F4 => EKey::F4,
93        Key::F5 => EKey::F5,
94        Key::F6 => EKey::F6,
95        Key::F7 => EKey::F7,
96        Key::F8 => EKey::F8,
97        Key::F9 => EKey::F9,
98        Key::F10 => EKey::F10,
99        Key::F11 => EKey::F11,
100        Key::F12 => EKey::F12,
101        Key::Equal => EKey::Equals,
102        Key::Hyphen => EKey::Minus,
103        Key::Slash => EKey::Slash,
104        Key::Tilde => EKey::Backtick,
105        _ => return None,
106    })
107}
108
109fn modifier(alt: bool, ctrl: bool, shift: bool) -> egui::Modifiers {
110    egui::Modifiers {
111        alt,
112        ctrl,
113        shift,
114        command: ctrl,
115        mac_cmd: false,
116    }
117}
118
119/// Converts an SFML event to an egui event and adds it to the `RawInput`.
120fn handle_event(
121    raw_input: &mut egui::RawInput,
122    event: &sf2g::window::Event,
123    clipboard: &mut arboard::Clipboard,
124) {
125    match *event {
126        Event::KeyPressed {
127            code,
128            alt,
129            ctrl,
130            shift,
131            system: _,
132            scan: _,
133        } => {
134            if ctrl {
135                match code {
136                    Key::V => match clipboard.get_text() {
137                        Ok(text) => raw_input.events.push(egui::Event::Text(text)),
138                        Err(e) => {
139                            eprintln!("[egui-sf2g] Paste failed: {e}");
140                        }
141                    },
142                    Key::C => raw_input.events.push(egui::Event::Copy),
143                    Key::X => raw_input.events.push(egui::Event::Cut),
144                    _ => {}
145                }
146            }
147            if let Some(key) = key_conv(code) {
148                raw_input.events.push(egui::Event::Key {
149                    key,
150                    modifiers: modifier(alt, ctrl, shift),
151                    pressed: true,
152                    repeat: false,
153                    physical_key: None,
154                });
155            }
156        }
157        Event::KeyReleased {
158            code,
159            alt,
160            ctrl,
161            shift,
162            system: _,
163            scan: _,
164        } => {
165            if let Some(key) = key_conv(code) {
166                raw_input.events.push(egui::Event::Key {
167                    key,
168                    modifiers: modifier(alt, ctrl, shift),
169                    pressed: false,
170                    repeat: false,
171                    physical_key: None,
172                });
173            }
174        }
175        Event::MouseMoved { x, y } => {
176            raw_input
177                .events
178                .push(egui::Event::PointerMoved(Pos2::new(x as f32, y as f32)));
179        }
180        Event::MouseButtonPressed { x, y, button } => {
181            if let Some(button) = button_conv(button) {
182                raw_input.events.push(egui::Event::PointerButton {
183                    pos: Pos2::new(x as f32, y as f32),
184                    button,
185                    pressed: true,
186                    modifiers: Modifiers::default(),
187                });
188            }
189        }
190        Event::MouseButtonReleased { x, y, button } => {
191            if let Some(button) = button_conv(button) {
192                raw_input.events.push(egui::Event::PointerButton {
193                    pos: Pos2::new(x as f32, y as f32),
194                    button,
195                    pressed: false,
196                    modifiers: Modifiers::default(),
197                });
198            }
199        }
200        Event::TextEntered { unicode } => {
201            if !unicode.is_control() {
202                raw_input
203                    .events
204                    .push(egui::Event::Text(unicode.to_string()));
205            }
206        }
207        Event::MouseWheelScrolled { delta, .. } => {
208            if sf2g::window::Key::LControl.is_pressed() {
209                raw_input
210                    .events
211                    .push(egui::Event::Zoom(if delta > 0.0 { 1.1 } else { 0.9 }));
212            } else {
213                raw_input.events.push(egui::Event::MouseWheel {
214                    unit: egui::MouseWheelUnit::Line,
215                    delta: egui::vec2(0.0, delta),
216                    modifiers: egui::Modifiers::default(),
217                });
218            }
219        }
220        Event::Resized { width, height } => {
221            raw_input.screen_rect = Some(raw_input_screen_rect(width, height));
222        }
223        _ => {}
224    }
225}
226
227/// Creates a `RawInput` that fits the window.
228fn make_raw_input(window: &RenderWindow) -> RawInput {
229    let Vector2 { x: w, y: h } = window.size();
230    RawInput {
231        screen_rect: Some(raw_input_screen_rect(w, h)),
232        max_texture_side: Some(Texture::maximum_size() as usize),
233        ..Default::default()
234    }
235}
236
237fn raw_input_screen_rect(w: u32, h: u32) -> egui::Rect {
238    egui::Rect {
239        min: Pos2::new(0., 0.),
240        max: Pos2::new(w as f32, h as f32),
241    }
242}
243
244/// A source for egui user textures.
245///
246/// You can create a struct that contains all the necessary information to get a user texture from
247/// an id, and implement this trait for it.
248pub trait UserTexSource {
249    /// Get the texture that corresponds to `id`.
250    ///
251    /// Returns (width, height, texture).
252    fn get_texture(&mut self, id: u64) -> (f32, f32, &Texture);
253}
254
255/// A dummy texture source in case you don't care about providing user textures
256struct DummyTexSource {
257    tex: FBox<Texture>,
258}
259
260impl Default for DummyTexSource {
261    fn default() -> Self {
262        Self {
263            tex: Texture::new().unwrap(),
264        }
265    }
266}
267
268impl UserTexSource for DummyTexSource {
269    fn get_texture(&mut self, _id: u64) -> (f32, f32, &Texture) {
270        (0., 0., &self.tex)
271    }
272}
273
274type TextureMap = HashMap<TextureId, FBox<Texture>>;
275
276/// `Egui` integration for SFML.
277pub struct SfEgui {
278    clock: FBox<Clock>,
279    ctx: Context,
280    raw_input: RawInput,
281    textures: TextureMap,
282    last_window_pos: Vector2i,
283    cursors: Cursors,
284    clipboard: arboard::Clipboard,
285}
286
287struct Cursors {
288    arrow: FBox<Cursor>,
289    horizontal: FBox<Cursor>,
290    vertical: FBox<Cursor>,
291    hand: FBox<Cursor>,
292    cross: FBox<Cursor>,
293    text: FBox<Cursor>,
294}
295
296impl Default for Cursors {
297    fn default() -> Self {
298        Self {
299            arrow: Cursor::from_system(CursorType::Arrow).unwrap(),
300            horizontal: Cursor::from_system(CursorType::SizeHorizontal).unwrap(),
301            vertical: Cursor::from_system(CursorType::SizeVertical).unwrap(),
302            hand: Cursor::from_system(CursorType::Hand).unwrap(),
303            cross: Cursor::from_system(CursorType::Cross).unwrap(),
304            text: Cursor::from_system(CursorType::Text).unwrap(),
305        }
306    }
307}
308
309/// Data required to draw the egui ui
310pub struct DrawInput {
311    shapes: Vec<egui::epaint::ClippedShape>,
312    pixels_per_point: f32,
313}
314
315impl SfEgui {
316    /// Create a new `SfEgui`.
317    ///
318    /// The size of the egui ui will be the same as `window`'s size.
319    pub fn new(window: &RenderWindow) -> Self {
320        Self {
321            clock: sf2g::system::Clock::start().unwrap(),
322            raw_input: make_raw_input(window),
323            ctx: Context::default(),
324            textures: TextureMap::default(),
325            last_window_pos: Vector2i::default(),
326            cursors: Cursors::default(),
327            clipboard: arboard::Clipboard::new().unwrap(),
328        }
329    }
330    /// Convert an SFML event into an egui event and add it for later use by egui.
331    ///
332    /// Call this in an event polling loop for each event.
333    pub fn add_event(&mut self, event: &Event) {
334        handle_event(&mut self.raw_input, event, &mut self.clipboard);
335    }
336    /// Does a [`egui::Context::run`] to run your egui ui.
337    ///
338    /// This supports egui uis that depend on multiple passes.
339    ///
340    /// See [`egui::Context::request_discard`].
341    ///
342    /// The `f` parameter is a user supplied ui function that does the desired ui
343    pub fn run(
344        &mut self,
345        rw: &mut RenderWindow,
346        mut f: impl FnMut(&mut RenderWindow, &Context),
347    ) -> Result<DrawInput, PassError> {
348        self.prepare_raw_input();
349        let out = self.ctx.run(self.raw_input.take(), |ctx| f(rw, ctx));
350        self.handle_output(
351            rw,
352            out.platform_output,
353            out.textures_delta,
354            out.viewport_output,
355        )?;
356        Ok(DrawInput {
357            shapes: out.shapes,
358            pixels_per_point: out.pixels_per_point,
359        })
360    }
361
362    /// Begins a (single) egui pass.
363    ///
364    /// This does not support egui uis that depend on multiple passes.
365    /// Use [`Self::run`] for that.
366    ///
367    /// If you call this, it should be paired with [`Self::end_pass`].
368    pub fn begin_pass(&mut self) {
369        self.prepare_raw_input();
370        self.ctx.begin_pass(self.raw_input.take());
371    }
372
373    /// Ends an egui pass. Call [`Self::begin_pass`] first.
374    pub fn end_pass(&mut self, rw: &mut RenderWindow) -> Result<DrawInput, PassError> {
375        let out = self.ctx.end_pass();
376        self.handle_output(
377            rw,
378            out.platform_output,
379            out.textures_delta,
380            out.viewport_output,
381        )?;
382        Ok(DrawInput {
383            shapes: out.shapes,
384            pixels_per_point: out.pixels_per_point,
385        })
386    }
387
388    fn handle_output(
389        &mut self,
390        rw: &mut RenderWindow,
391        platform_output: egui::PlatformOutput,
392        textures_delta: egui::TexturesDelta,
393        viewport_output: egui::OrderedViewportIdMap<egui::ViewportOutput>,
394    ) -> Result<(), PassError> {
395        for (id, delta) in &textures_delta.set {
396            let tex = self
397                .textures
398                .entry(*id)
399                .or_insert_with(|| Texture::new().unwrap());
400            rendering::update_tex_from_delta(tex, delta)?;
401        }
402        for id in &textures_delta.free {
403            self.textures.remove(id);
404        }
405        let new_cursor = match platform_output.cursor_icon {
406            CursorIcon::Default => Some(&self.cursors.arrow),
407            CursorIcon::None => None,
408            CursorIcon::PointingHand | CursorIcon::Grab | CursorIcon::Grabbing => {
409                Some(&self.cursors.hand)
410            }
411            CursorIcon::Crosshair => Some(&self.cursors.cross),
412            CursorIcon::Text => Some(&self.cursors.text),
413            CursorIcon::ResizeHorizontal | CursorIcon::ResizeColumn => {
414                Some(&self.cursors.horizontal)
415            }
416            CursorIcon::ResizeVertical => Some(&self.cursors.vertical),
417            _ => Some(&self.cursors.arrow),
418        };
419        match new_cursor {
420            Some(cur) => {
421                rw.set_mouse_cursor_visible(true);
422                unsafe {
423                    rw.set_mouse_cursor(cur);
424                }
425            }
426            None => rw.set_mouse_cursor_visible(false),
427        }
428        for cmd in platform_output.commands {
429            match cmd {
430                egui::OutputCommand::CopyText(txt) => {
431                    if let Err(e) = self.clipboard.set_text(txt) {
432                        eprintln!("[egui-sf2g] Failed to set clipboard text: {e}");
433                    }
434                }
435                egui::OutputCommand::CopyImage(_img) => {
436                    eprintln!("egui-sf2g: Unimplemented image copy");
437                }
438                egui::OutputCommand::OpenUrl(_url) => {
439                    eprintln!("egui-sf2g: Unimplemented url open");
440                }
441            }
442        }
443        // TODO: Multi-viewport support
444        for (_, out) in viewport_output {
445            for cmd in out.commands {
446                match cmd {
447                    ViewportCommand::Close => rw.close(),
448                    ViewportCommand::Title(s) => rw.set_title(&s),
449                    ViewportCommand::Visible(visible) => {
450                        if !visible {
451                            self.last_window_pos = rw.position();
452                        }
453                        rw.set_visible(visible);
454                        if visible {
455                            rw.set_position(self.last_window_pos);
456                        }
457                    }
458                    ViewportCommand::Focus => {
459                        // This trick forces focus where `request_focus` would
460                        // only flash the tray icon.
461                        let rw_pos = rw.position();
462                        rw.set_visible(false);
463                        rw.set_visible(true);
464                        rw.set_position(rw_pos);
465                    }
466                    _ => eprintln!("egui_sf2g: Unhandled ViewportCommand: {cmd:?}"),
467                }
468            }
469        }
470        Ok(())
471    }
472
473    fn prepare_raw_input(&mut self) {
474        self.raw_input.time = Some(self.clock.elapsed_time().as_seconds() as f64);
475        // Update modifiers every frame, otherwise querying them (input.modifiers.*) doesn't seem
476        // up-to-date
477        self.raw_input.modifiers.alt = Key::LAlt.is_pressed() || Key::RAlt.is_pressed();
478        self.raw_input.modifiers.ctrl = Key::LControl.is_pressed() || Key::RControl.is_pressed();
479        self.raw_input.modifiers.shift = Key::LShift.is_pressed() || Key::RShift.is_pressed();
480    }
481    /// Draw the ui to a `RenderWindow`.
482    ///
483    /// Takes an optional [`UserTexSource`] to act as a user texture source.
484    pub fn draw(
485        &mut self,
486        input: DrawInput,
487        window: &mut RenderWindow,
488        user_tex_src: Option<&mut dyn UserTexSource>,
489    ) {
490        rendering::draw(
491            window,
492            &self.ctx,
493            input.shapes,
494            user_tex_src.unwrap_or(&mut DummyTexSource::default()),
495            &self.textures,
496            input.pixels_per_point,
497        )
498    }
499    /// Returns a handle to the egui context
500    pub fn context(&self) -> &Context {
501        &self.ctx
502    }
503}
504
505#[derive(Debug)]
506/// Error when failing to create a texture
507pub struct TextureCreateError {
508    /// The width of the requested texture
509    pub width: usize,
510    /// The height of the requested texture
511    pub height: usize,
512}
513
514impl std::fmt::Display for TextureCreateError {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        let (width, height) = (self.width, self.height);
517        f.write_fmt(format_args!(
518            "Failed to create texture of size {width}x{height}"
519        ))
520    }
521}
522
523/// Error that can happen during an egui pass
524#[non_exhaustive]
525#[derive(Debug)]
526pub enum PassError {
527    /// Failed to create a texture
528    TextureCreateError(TextureCreateError),
529}
530
531impl From<TextureCreateError> for PassError {
532    fn from(src: TextureCreateError) -> Self {
533        Self::TextureCreateError(src)
534    }
535}
536
537impl std::fmt::Display for PassError {
538    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539        match self {
540            PassError::TextureCreateError(e) => {
541                f.write_fmt(format_args!("Texture create error: {e}"))
542            }
543        }
544    }
545}
546
547impl std::error::Error for PassError {}