1#![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
119fn 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
227fn 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
244pub trait UserTexSource {
249 fn get_texture(&mut self, id: u64) -> (f32, f32, &Texture);
253}
254
255struct 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
276pub 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
309pub struct DrawInput {
311 shapes: Vec<egui::epaint::ClippedShape>,
312 pixels_per_point: f32,
313}
314
315impl SfEgui {
316 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 pub fn add_event(&mut self, event: &Event) {
334 handle_event(&mut self.raw_input, event, &mut self.clipboard);
335 }
336 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 pub fn begin_pass(&mut self) {
369 self.prepare_raw_input();
370 self.ctx.begin_pass(self.raw_input.take());
371 }
372
373 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 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 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 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 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 pub fn context(&self) -> &Context {
501 &self.ctx
502 }
503}
504
505#[derive(Debug)]
506pub struct TextureCreateError {
508 pub width: usize,
510 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#[non_exhaustive]
525#[derive(Debug)]
526pub enum PassError {
527 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 {}