Skip to main content

ry_backend/
sdl2_core.rs

1//! Módulo SDL2 Core - Input + Audio + Assets + Texto TTF profesional
2//!
3//! SDL2 es excelente para:
4//! - Texto TTF profesional: anti-alias, fuentes reales, kerning
5//! - Input completo: teclado, mouse, touch, gamepad
6//! - Audio: SDL2_mixer (WAV, OGG, MP3)
7//! - Assets: SDL2_image (PNG, JPG, BMP)
8
9use sdl2::event::Event;
10use sdl2::pixels::Color as SdlColor;
11use sdl2::rect::Rect;
12use sdl2::render::{Canvas, Texture, TextureCreator};
13use sdl2::surface::Surface;
14use sdl2::video::Window;
15use sdl2::image::LoadSurface;
16
17// ============================================================================
18// EVENTOS DE MOUSE COMPLETOS
19// ============================================================================
20
21#[derive(Debug, Clone)]
22pub enum MouseEvent {
23    Moved { x: i32, y: i32, dx: i32, dy: i32 },
24    LeftClick { x: i32, y: i32 },
25    LeftDoubleClick { x: i32, y: i32 },
26    RightClick { x: i32, y: i32 },
27    RightDoubleClick { x: i32, y: i32 },
28    MiddleClick { x: i32, y: i32 },
29    ButtonDown { x: i32, y: i32, button: sdl2::mouse::MouseButton },
30    ButtonUp { x: i32, y: i32, button: sdl2::mouse::MouseButton },
31    Wheel { x: i32, y: i32, direction: MouseWheelDirection },
32    EnterWindow,
33    LeaveWindow,
34}
35
36#[derive(Debug, Clone)]
37pub enum MouseWheelDirection { Normal, Flipped }
38
39pub struct MouseState {
40    pub x: i32, pub y: i32,
41    pub left_down: bool, pub right_down: bool, pub middle_down: bool,
42    pub scroll_x: i32, pub scroll_y: i32,
43    last_click_time: std::time::Instant,
44    last_click_pos: (i32, i32),
45}
46
47impl MouseState {
48    pub fn new() -> Self {
49        Self { x: 0, y: 0, left_down: false, right_down: false, middle_down: false,
50               scroll_x: 0, scroll_y: 0,
51               last_click_time: std::time::Instant::now(), last_click_pos: (0, 0) }
52    }
53
54    pub fn process_event(&mut self, event: &Event) -> Option<MouseEvent> {
55        match event {
56            Event::MouseMotion { x, y, xrel, yrel, .. } => {
57                self.x = *x; self.y = *y;
58                Some(MouseEvent::Moved { x: *x, y: *y, dx: *xrel, dy: *yrel })
59            }
60            Event::MouseButtonDown { mouse_btn, x, y, clicks, .. } => {
61                self.x = *x; self.y = *y;
62                let now = std::time::Instant::now();
63                let is_double = now.duration_since(self.last_click_time) < std::time::Duration::from_millis(500)
64                    && *x == self.last_click_pos.0 && *y == self.last_click_pos.1 && *clicks == 2;
65                self.last_click_time = now; self.last_click_pos = (*x, *y);
66                match mouse_btn {
67                    sdl2::mouse::MouseButton::Left => {
68                        self.left_down = true;
69                        Some(if is_double { MouseEvent::LeftDoubleClick { x: *x, y: *y } }
70                             else { MouseEvent::LeftClick { x: *x, y: *y } })
71                    }
72                    sdl2::mouse::MouseButton::Right => {
73                        self.right_down = true;
74                        Some(if is_double { MouseEvent::RightDoubleClick { x: *x, y: *y } }
75                             else { MouseEvent::RightClick { x: *x, y: *y } })
76                    }
77                    sdl2::mouse::MouseButton::Middle => {
78                        self.middle_down = true;
79                        Some(MouseEvent::MiddleClick { x: *x, y: *y })
80                    }
81                    _ => Some(MouseEvent::ButtonDown { x: *x, y: *y, button: *mouse_btn }),
82                }
83            }
84            Event::MouseButtonUp { mouse_btn, x, y, .. } => {
85                self.x = *x; self.y = *y;
86                match mouse_btn {
87                    sdl2::mouse::MouseButton::Left => self.left_down = false,
88                    sdl2::mouse::MouseButton::Right => self.right_down = false,
89                    sdl2::mouse::MouseButton::Middle => self.middle_down = false,
90                    _ => {}
91                }
92                Some(MouseEvent::ButtonUp { x: *x, y: *y, button: *mouse_btn })
93            }
94            Event::MouseWheel { x, y, direction, .. } => {
95                self.scroll_x = *x; self.scroll_y = *y;
96                let dir = match direction {
97                    sdl2::mouse::MouseWheelDirection::Normal => MouseWheelDirection::Normal,
98                    sdl2::mouse::MouseWheelDirection::Flipped => MouseWheelDirection::Flipped,
99                    _ => MouseWheelDirection::Normal,
100                };
101                Some(MouseEvent::Wheel { x: *x, y: *y, direction: dir })
102            }
103            Event::Window { win_event: sdl2::event::WindowEvent::Leave, .. } => Some(MouseEvent::LeaveWindow),
104            Event::Window { win_event: sdl2::event::WindowEvent::Enter, .. } => Some(MouseEvent::EnterWindow),
105            _ => None,
106        }
107    }
108}
109
110impl Default for MouseState { fn default() -> Self { Self::new() } }
111
112// ============================================================================
113// TEXTO TTF PROFESIONAL
114// ============================================================================
115
116pub struct TextTexture {
117    pub texture: Texture<'static>,
118    pub width: u32,
119    pub height: u32,
120}
121
122pub struct TtfFont {
123    texture_creator: std::rc::Rc<TextureCreator<sdl2::video::WindowContext>>,
124    font_path: String,
125    font_size: u16,
126}
127
128impl TtfFont {
129    pub fn new(path: String, size: u16, tc: std::rc::Rc<TextureCreator<sdl2::video::WindowContext>>) -> Self {
130        Self { texture_creator: tc, font_path: path, font_size: size }
131    }
132
133    pub fn render_text(&self, text: &str, color: SdlColor) -> Option<TextTexture> {
134        let ttf = sdl2::ttf::init().ok()?;
135        let font = ttf.load_font(&self.font_path, self.font_size).ok()?;
136        let surface = font.render(text).blended(color).ok()?;
137        let (w, h) = surface.size();
138        let texture = self.texture_creator.create_texture_from_surface(&surface).ok()?;
139        let st: Texture<'static> = unsafe { std::mem::transmute(texture) };
140        Some(TextTexture { texture: st, width: w, height: h })
141    }
142
143    pub fn measure_text(&self, text: &str) -> (u32, u32) {
144        if let Ok(ttf) = sdl2::ttf::init() {
145            if let Ok(font) = ttf.load_font(&self.font_path, self.font_size) {
146                return font.size_of(text).unwrap_or((0, 0));
147            }
148        }
149        (text.len() as u32 * 8, self.font_size as u32)
150    }
151}
152
153// ============================================================================
154// SDL2 CORE
155// ============================================================================
156
157pub struct Sdl2Core {
158    pub canvas: Canvas<Window>,
159    pub event_pump: sdl2::EventPump,
160    pub mouse: MouseState,
161    pub texture_creator: std::rc::Rc<TextureCreator<sdl2::video::WindowContext>>,
162    pub font: Option<TtfFont>,
163    pub width: i32, pub height: i32,
164}
165
166impl Sdl2Core {
167    pub fn new(title: &str, width: i32, height: i32) -> Result<Self, String> {
168        sdl2::hint::set("SDL_VIDEODRIVER", "x11");
169        sdl2::hint::set("SDL_RENDER_DRIVER", "opengles2");
170        sdl2::hint::set("SDL_VIDEO_X11_FORCE_EGL", "1");
171        sdl2::hint::set("SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH", "1");
172        sdl2::hint::set("SDL_HINT_TOUCH_MOUSE_EVENTS", "1");
173        sdl2::hint::set("SDL_HINT_ENABLE_SCREEN_KEYBOARD", "1");
174        sdl2::hint::set("SDL_HINT_IME_SHOW_UI", "1");
175
176        let sdl = sdl2::init().map_err(|e| e.to_string())?;
177        let video = sdl.video().map_err(|e| e.to_string())?;
178        sdl2::ttf::init().map_err(|e| e.to_string())?;
179        unsafe { sdl2::sys::SDL_StartTextInput(); }
180
181        let window = video.window(title, width as u32, height as u32)
182            .position_centered().opengl().build().map_err(|e| e.to_string())?;
183        let canvas = window.into_canvas().accelerated().present_vsync()
184            .build().map_err(|e| e.to_string())?;
185        let texture_creator = std::rc::Rc::new(canvas.texture_creator());
186        let event_pump = sdl.event_pump().map_err(|e| e.to_string())?;
187
188        let font = Self::load_system_font(std::rc::Rc::clone(&texture_creator));
189
190        Ok(Self { canvas, event_pump, mouse: MouseState::new(), texture_creator, font, width, height })
191    }
192
193    fn load_system_font(tc: std::rc::Rc<TextureCreator<sdl2::video::WindowContext>>) -> Option<TtfFont> {
194        for path in &["/system/fonts/Roboto-Regular.ttf", "/system/fonts/DroidSans.ttf",
195                       "/data/data/com.termux/files/usr/share/fonts/DejaVuSans.ttf"] {
196            if std::path::Path::new(path).exists() {
197                println!("[SDL2-TTF] Fuente: {}", path);
198                return Some(TtfFont::new((*path).into(), 16, tc));
199            }
200        }
201        eprintln!("[SDL2-TTF] Sin fuente del sistema");
202        None
203    }
204
205    pub fn poll_events(&mut self) -> Vec<MouseEvent> {
206        let mut evts = Vec::new();
207        for event in self.event_pump.poll_iter() {
208            if let Some(me) = self.mouse.process_event(&event) { evts.push(me); }
209        }
210        evts
211    }
212
213    pub fn draw_text(&mut self, text: &str, x: i32, y: i32, color: SdlColor) {
214        if let Some(ref font) = self.font {
215            if let Some(tex) = font.render_text(text, color) {
216                let _ = self.canvas.copy(&tex.texture, None, Rect::new(x, y, tex.width, tex.height));
217            }
218        }
219    }
220
221    pub fn load_sprite(&self, path: &str) -> Option<Sprite> {
222        Sprite::load(&self.canvas, path).ok()
223    }
224}
225
226// ============================================================================
227// SPRITE
228// ============================================================================
229
230pub struct Sprite {
231    texture: Texture<'static>,
232    width: u32, height: u32,
233}
234
235impl Sprite {
236    pub fn load(canvas: &Canvas<Window>, path: &str) -> Result<Self, String> {
237        let tc = canvas.texture_creator();
238        let surface: Surface<'_> = Surface::from_file(path).map_err(|e| e.to_string())?;
239        let (w, h) = surface.size();
240        let texture = tc.create_texture_from_surface(&surface).map_err(|e| e.to_string())?;
241        let st: Texture<'static> = unsafe { std::mem::transmute(texture) };
242        Ok(Self { texture: st, width: w, height: h })
243    }
244
245    pub fn draw(&self, canvas: &mut Canvas<Window>, x: i32, y: i32, scale: u32) {
246        let _ = canvas.copy(&self.texture, None, Rect::new(x, y, self.width * scale, self.height * scale));
247    }
248
249    pub fn size(&self) -> (u32, u32) { (self.width, self.height) }
250}