egui_sfml/
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, sfml};
10use {
11    egui::{
12        Context, CursorIcon, Modifiers, PointerButton, Pos2, RawInput, TextureId, ViewportCommand,
13    },
14    sfml::{
15        cpp::FBox,
16        graphics::{RenderTarget as _, RenderWindow, Texture},
17        system::{Clock, Vector2, Vector2i},
18        window::{clipboard, mouse, Cursor, CursorType, Event, Key},
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(raw_input: &mut egui::RawInput, event: &sfml::window::Event) {
121    match *event {
122        Event::KeyPressed {
123            code,
124            alt,
125            ctrl,
126            shift,
127            system: _,
128            scan: _,
129        } => {
130            if ctrl {
131                match code {
132                    Key::V => raw_input
133                        .events
134                        .push(egui::Event::Text(clipboard::get_string())),
135                    Key::C => raw_input.events.push(egui::Event::Copy),
136                    Key::X => raw_input.events.push(egui::Event::Cut),
137                    _ => {}
138                }
139            }
140            if let Some(key) = key_conv(code) {
141                raw_input.events.push(egui::Event::Key {
142                    key,
143                    modifiers: modifier(alt, ctrl, shift),
144                    pressed: true,
145                    repeat: false,
146                    physical_key: None,
147                });
148            }
149        }
150        Event::KeyReleased {
151            code,
152            alt,
153            ctrl,
154            shift,
155            system: _,
156            scan: _,
157        } => {
158            if let Some(key) = key_conv(code) {
159                raw_input.events.push(egui::Event::Key {
160                    key,
161                    modifiers: modifier(alt, ctrl, shift),
162                    pressed: false,
163                    repeat: false,
164                    physical_key: None,
165                });
166            }
167        }
168        Event::MouseMoved { x, y } => {
169            raw_input
170                .events
171                .push(egui::Event::PointerMoved(Pos2::new(x as f32, y as f32)));
172        }
173        Event::MouseButtonPressed { x, y, button } => {
174            if let Some(button) = button_conv(button) {
175                raw_input.events.push(egui::Event::PointerButton {
176                    pos: Pos2::new(x as f32, y as f32),
177                    button,
178                    pressed: true,
179                    modifiers: Modifiers::default(),
180                });
181            }
182        }
183        Event::MouseButtonReleased { x, y, button } => {
184            if let Some(button) = button_conv(button) {
185                raw_input.events.push(egui::Event::PointerButton {
186                    pos: Pos2::new(x as f32, y as f32),
187                    button,
188                    pressed: false,
189                    modifiers: Modifiers::default(),
190                });
191            }
192        }
193        Event::TextEntered { unicode } => {
194            if !unicode.is_control() {
195                raw_input
196                    .events
197                    .push(egui::Event::Text(unicode.to_string()));
198            }
199        }
200        Event::MouseWheelScrolled { delta, .. } => {
201            if sfml::window::Key::LControl.is_pressed() {
202                raw_input
203                    .events
204                    .push(egui::Event::Zoom(if delta > 0.0 { 1.1 } else { 0.9 }));
205            }
206        }
207        Event::Resized { width, height } => {
208            raw_input.screen_rect = Some(raw_input_screen_rect(width, height));
209        }
210        _ => {}
211    }
212}
213
214/// Creates a `RawInput` that fits the window.
215fn make_raw_input(window: &RenderWindow) -> RawInput {
216    let Vector2 { x: w, y: h } = window.size();
217    RawInput {
218        screen_rect: Some(raw_input_screen_rect(w, h)),
219        max_texture_side: Some(Texture::maximum_size() as usize),
220        ..Default::default()
221    }
222}
223
224fn raw_input_screen_rect(w: u32, h: u32) -> egui::Rect {
225    egui::Rect {
226        min: Pos2::new(0., 0.),
227        max: Pos2::new(w as f32, h as f32),
228    }
229}
230
231/// A source for egui user textures.
232///
233/// You can create a struct that contains all the necessary information to get a user texture from
234/// an id, and implement this trait for it.
235pub trait UserTexSource {
236    /// Get the texture that corresponds to `id`.
237    ///
238    /// Returns (width, height, texture).
239    fn get_texture(&mut self, id: u64) -> (f32, f32, &Texture);
240}
241
242/// A dummy texture source in case you don't care about providing user textures
243struct DummyTexSource {
244    tex: FBox<Texture>,
245}
246
247impl Default for DummyTexSource {
248    fn default() -> Self {
249        Self {
250            tex: Texture::new().unwrap(),
251        }
252    }
253}
254
255impl UserTexSource for DummyTexSource {
256    fn get_texture(&mut self, _id: u64) -> (f32, f32, &Texture) {
257        (0., 0., &self.tex)
258    }
259}
260
261type TextureMap = HashMap<TextureId, FBox<Texture>>;
262
263/// `Egui` integration for SFML.
264pub struct SfEgui {
265    clock: FBox<Clock>,
266    ctx: Context,
267    raw_input: RawInput,
268    textures: TextureMap,
269    last_window_pos: Vector2i,
270    cursors: Cursors,
271}
272
273struct Cursors {
274    arrow: FBox<Cursor>,
275    horizontal: FBox<Cursor>,
276    vertical: FBox<Cursor>,
277    hand: FBox<Cursor>,
278    cross: FBox<Cursor>,
279    text: FBox<Cursor>,
280}
281
282impl Default for Cursors {
283    fn default() -> Self {
284        Self {
285            arrow: Cursor::from_system(CursorType::Arrow).unwrap(),
286            horizontal: Cursor::from_system(CursorType::SizeHorizontal).unwrap(),
287            vertical: Cursor::from_system(CursorType::SizeVertical).unwrap(),
288            hand: Cursor::from_system(CursorType::Hand).unwrap(),
289            cross: Cursor::from_system(CursorType::Cross).unwrap(),
290            text: Cursor::from_system(CursorType::Text).unwrap(),
291        }
292    }
293}
294
295/// Data required to draw the egui ui
296pub struct DrawInput {
297    shapes: Vec<egui::epaint::ClippedShape>,
298    pixels_per_point: f32,
299}
300
301impl SfEgui {
302    /// Create a new `SfEgui`.
303    ///
304    /// The size of the egui ui will be the same as `window`'s size.
305    pub fn new(window: &RenderWindow) -> Self {
306        Self {
307            clock: sfml::system::Clock::start().unwrap(),
308            raw_input: make_raw_input(window),
309            ctx: Context::default(),
310            textures: TextureMap::default(),
311            last_window_pos: Vector2i::default(),
312            cursors: Cursors::default(),
313        }
314    }
315    /// Convert an SFML event into an egui event and add it for later use by egui.
316    ///
317    /// Call this in an event polling loop for each event.
318    pub fn add_event(&mut self, event: &Event) {
319        handle_event(&mut self.raw_input, event);
320    }
321    /// Does a [`egui::Context::run`] to run your egui ui.
322    ///
323    /// This supports egui uis that depend on multiple passes.
324    ///
325    /// See [`egui::Context::request_discard`].
326    ///
327    /// The `f` parameter is a user supplied ui function that does the desired ui
328    pub fn run(
329        &mut self,
330        rw: &mut RenderWindow,
331        mut f: impl FnMut(&mut RenderWindow, &Context),
332    ) -> Result<DrawInput, PassError> {
333        self.prepare_raw_input();
334        let out = self.ctx.run(self.raw_input.take(), |ctx| f(rw, ctx));
335        self.handle_output(
336            rw,
337            out.platform_output,
338            out.textures_delta,
339            out.viewport_output,
340        )?;
341        Ok(DrawInput {
342            shapes: out.shapes,
343            pixels_per_point: out.pixels_per_point,
344        })
345    }
346
347    /// Begins a (single) egui pass.
348    ///
349    /// This does not support egui uis that depend on multiple passes.
350    /// Use [`Self::run`] for that.
351    ///
352    /// If you call this, it should be paired with [`Self::end_pass`].
353    pub fn begin_pass(&mut self) {
354        self.prepare_raw_input();
355        self.ctx.begin_pass(self.raw_input.take());
356    }
357
358    /// Ends an egui pass. Call [`Self::begin_pass`] first.
359    pub fn end_pass(&mut self, rw: &mut RenderWindow) -> Result<DrawInput, PassError> {
360        let out = self.ctx.end_pass();
361        self.handle_output(
362            rw,
363            out.platform_output,
364            out.textures_delta,
365            out.viewport_output,
366        )?;
367        Ok(DrawInput {
368            shapes: out.shapes,
369            pixels_per_point: out.pixels_per_point,
370        })
371    }
372
373    fn handle_output(
374        &mut self,
375        rw: &mut RenderWindow,
376        platform_output: egui::PlatformOutput,
377        textures_delta: egui::TexturesDelta,
378        viewport_output: egui::ViewportIdMap<egui::ViewportOutput>,
379    ) -> Result<(), PassError> {
380        for (id, delta) in &textures_delta.set {
381            let tex = self
382                .textures
383                .entry(*id)
384                .or_insert_with(|| Texture::new().unwrap());
385            rendering::update_tex_from_delta(tex, delta)?;
386        }
387        for id in &textures_delta.free {
388            self.textures.remove(id);
389        }
390        let new_cursor = match platform_output.cursor_icon {
391            CursorIcon::Default => Some(&self.cursors.arrow),
392            CursorIcon::None => None,
393            CursorIcon::PointingHand | CursorIcon::Grab | CursorIcon::Grabbing => {
394                Some(&self.cursors.hand)
395            }
396            CursorIcon::Crosshair => Some(&self.cursors.cross),
397            CursorIcon::Text => Some(&self.cursors.text),
398            CursorIcon::ResizeHorizontal | CursorIcon::ResizeColumn => {
399                Some(&self.cursors.horizontal)
400            }
401            CursorIcon::ResizeVertical => Some(&self.cursors.vertical),
402            _ => Some(&self.cursors.arrow),
403        };
404        match new_cursor {
405            Some(cur) => {
406                rw.set_mouse_cursor_visible(true);
407                unsafe {
408                    rw.set_mouse_cursor(cur);
409                }
410            }
411            None => rw.set_mouse_cursor_visible(false),
412        }
413        for cmd in platform_output.commands {
414            match cmd {
415                egui::OutputCommand::CopyText(txt) => {
416                    clipboard::set_string(&txt);
417                }
418                egui::OutputCommand::CopyImage(_img) => {
419                    eprintln!("egui-sfml: Unimplemented image copy");
420                }
421                egui::OutputCommand::OpenUrl(_url) => {
422                    eprintln!("egui-sfml: Unimplemented url open");
423                }
424            }
425        }
426        // TODO: Multi-viewport support
427        for (_, out) in viewport_output {
428            for cmd in out.commands {
429                match cmd {
430                    ViewportCommand::Close => rw.close(),
431                    ViewportCommand::Title(s) => rw.set_title(&s),
432                    ViewportCommand::Visible(visible) => {
433                        if !visible {
434                            self.last_window_pos = rw.position();
435                        }
436                        rw.set_visible(visible);
437                        if visible {
438                            rw.set_position(self.last_window_pos);
439                        }
440                    }
441                    ViewportCommand::Focus => {
442                        // This trick forces focus where `request_focus` would
443                        // only flash the tray icon.
444                        let rw_pos = rw.position();
445                        rw.set_visible(false);
446                        rw.set_visible(true);
447                        rw.set_position(rw_pos);
448                    }
449                    _ => eprintln!("egui_sfml: Unhandled ViewportCommand: {cmd:?}"),
450                }
451            }
452        }
453        Ok(())
454    }
455
456    fn prepare_raw_input(&mut self) {
457        self.raw_input.time = Some(self.clock.elapsed_time().as_seconds() as f64);
458        // Update modifiers every frame, otherwise querying them (input.modifiers.*) doesn't seem
459        // up-to-date
460        self.raw_input.modifiers.alt = Key::LAlt.is_pressed() || Key::RAlt.is_pressed();
461        self.raw_input.modifiers.ctrl = Key::LControl.is_pressed() || Key::RControl.is_pressed();
462        self.raw_input.modifiers.shift = Key::LShift.is_pressed() || Key::RShift.is_pressed();
463    }
464    /// Draw the ui to a `RenderWindow`.
465    ///
466    /// Takes an optional [`UserTexSource`] to act as a user texture source.
467    pub fn draw(
468        &mut self,
469        input: DrawInput,
470        window: &mut RenderWindow,
471        user_tex_src: Option<&mut dyn UserTexSource>,
472    ) {
473        rendering::draw(
474            window,
475            &self.ctx,
476            input.shapes,
477            user_tex_src.unwrap_or(&mut DummyTexSource::default()),
478            &self.textures,
479            input.pixels_per_point,
480        )
481    }
482    /// Returns a handle to the egui context
483    pub fn context(&self) -> &Context {
484        &self.ctx
485    }
486}
487
488#[derive(Debug)]
489/// Error when failing to create a texture
490pub struct TextureCreateError {
491    /// The width of the requested texture
492    pub width: usize,
493    /// The height of the requested texture
494    pub height: usize,
495}
496
497impl std::fmt::Display for TextureCreateError {
498    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499        let (width, height) = (self.width, self.height);
500        f.write_fmt(format_args!(
501            "Failed to create texture of size {width}x{height}"
502        ))
503    }
504}
505
506/// Error that can happen during an egui pass
507#[non_exhaustive]
508#[derive(Debug)]
509pub enum PassError {
510    /// Failed to create a texture
511    TextureCreateError(TextureCreateError),
512}
513
514impl From<TextureCreateError> for PassError {
515    fn from(src: TextureCreateError) -> Self {
516        Self::TextureCreateError(src)
517    }
518}
519
520impl std::fmt::Display for PassError {
521    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522        match self {
523            PassError::TextureCreateError(e) => {
524                f.write_fmt(format_args!("Texture create error: {e}"))
525            }
526        }
527    }
528}
529
530impl std::error::Error for PassError {}