1#![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
119fn 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
214fn 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
231pub trait UserTexSource {
236 fn get_texture(&mut self, id: u64) -> (f32, f32, &Texture);
240}
241
242struct 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
263pub 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
295pub struct DrawInput {
297 shapes: Vec<egui::epaint::ClippedShape>,
298 pixels_per_point: f32,
299}
300
301impl SfEgui {
302 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 pub fn add_event(&mut self, event: &Event) {
319 handle_event(&mut self.raw_input, event);
320 }
321 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 pub fn begin_pass(&mut self) {
354 self.prepare_raw_input();
355 self.ctx.begin_pass(self.raw_input.take());
356 }
357
358 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 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 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 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 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 pub fn context(&self) -> &Context {
484 &self.ctx
485 }
486}
487
488#[derive(Debug)]
489pub struct TextureCreateError {
491 pub width: usize,
493 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#[non_exhaustive]
508#[derive(Debug)]
509pub enum PassError {
510 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 {}