Skip to main content

toolkit_ry/
widgets.rs

1// crates/toolkit-ry/src/widgets.rs
2// Widgets de juego para Ry-Dit — HUD, Menús, Diálogos, Inventario
3// Construido sobre migui (immediate mode) + Theme system
4
5use crate::theme::{ColorRGBA, Theme};
6
7// ============================================================================
8// UTILIDADES DE COLOR
9// ============================================================================
10
11/// Convertir ColorRGBA a migui Color
12pub fn rgba_to_migui(c: ColorRGBA) -> migui::Color {
13    migui::Color {
14        r: c.r,
15        g: c.g,
16        b: c.b,
17        a: c.a,
18    }
19}
20
21/// Convertir ColorRGBA a SDL2 Color
22pub fn rgba_to_sdl2(c: ColorRGBA) -> sdl2::pixels::Color {
23    sdl2::pixels::Color::RGBA(c.r, c.g, c.b, c.a)
24}
25
26// ============================================================================
27// HUD WIDGETS
28// ============================================================================
29
30/// Barra de vida (HP)
31/// Retorna true si se renderizó correctamente
32pub fn draw_health_bar(
33    gui: &mut migui::Migui,
34    x: f32,
35    y: f32,
36    width: f32,
37    height: f32,
38    current: f32,
39    max: f32,
40    theme: &Theme,
41) {
42    let ratio = (current / max).clamp(0.0, 1.0);
43
44    // Fondo de la barra
45    gui.panel(
46        migui::WidgetId::new("hp_bg"),
47        migui::Rect::new(x, y, width, height),
48        rgba_to_migui(theme.health_bar_bg),
49    );
50
51    // Relleno de vida
52    let fill_width = width * ratio;
53    if fill_width > 0.0 {
54        gui.panel(
55            migui::WidgetId::new("hp_fill"),
56            migui::Rect::new(x, y, fill_width, height),
57            rgba_to_migui(theme.health_bar_fill),
58        );
59    }
60
61    // Borde
62    if theme.border_width > 0.0 {
63        gui.panel(
64            migui::WidgetId::new("hp_border"),
65            migui::Rect::new(x, y, width, height),
66            rgba_to_migui(ColorRGBA::transparent()),
67        );
68    }
69
70    // Texto "HP: 75/100"
71    let label = format!("HP: {}/{}", current as i32, max as i32);
72    gui.label(
73        migui::WidgetId::new("hp_label"),
74        &label,
75        migui::Rect::new(x + 4.0, y + 2.0, width - 8.0, height),
76    );
77}
78
79/// Barra de maná (MP)
80pub fn draw_mana_bar(
81    gui: &mut migui::Migui,
82    x: f32,
83    y: f32,
84    width: f32,
85    height: f32,
86    current: f32,
87    max: f32,
88    theme: &Theme,
89) {
90    let ratio = (current / max).clamp(0.0, 1.0);
91
92    gui.panel(
93        migui::WidgetId::new("mp_bg"),
94        migui::Rect::new(x, y, width, height),
95        rgba_to_migui(theme.mana_bar_bg),
96    );
97
98    let fill_width = width * ratio;
99    if fill_width > 0.0 {
100        gui.panel(
101            migui::WidgetId::new("mp_fill"),
102            migui::Rect::new(x, y, fill_width, height),
103            rgba_to_migui(theme.mana_bar_fill),
104        );
105    }
106
107    let label = format!("MP: {}/{}", current as i32, max as i32);
108    gui.label(
109        migui::WidgetId::new("mp_label"),
110        &label,
111        migui::Rect::new(x + 4.0, y + 2.0, width - 8.0, height),
112    );
113}
114
115/// Barra de experiencia (XP)
116pub fn draw_xp_bar(
117    gui: &mut migui::Migui,
118    x: f32,
119    y: f32,
120    width: f32,
121    height: f32,
122    current: f32,
123    max: f32,
124    theme: &Theme,
125) {
126    let ratio = (current / max).clamp(0.0, 1.0);
127
128    gui.panel(
129        migui::WidgetId::new("xp_bg"),
130        migui::Rect::new(x, y, width, height),
131        rgba_to_migui(theme.xp_bar_bg),
132    );
133
134    let fill_width = width * ratio;
135    if fill_width > 0.0 {
136        gui.panel(
137            migui::WidgetId::new("xp_fill"),
138            migui::Rect::new(x, y, fill_width, height),
139            rgba_to_migui(theme.xp_bar_fill),
140        );
141    }
142
143    let label = format!("XP: {}/{}", current as i32, max as i32);
144    gui.label(
145        migui::WidgetId::new("xp_label"),
146        &label,
147        migui::Rect::new(x + 4.0, y + 2.0, width - 8.0, height),
148    );
149}
150
151/// Display de puntuación
152pub fn draw_score(
153    gui: &mut migui::Migui,
154    x: f32,
155    y: f32,
156    score: i64,
157    theme: &Theme,
158) {
159    let label = format!("Score: {}", score);
160    gui.label(
161        migui::WidgetId::new("score"),
162        &label,
163        migui::Rect::new(x, y, 200.0, theme.font_size as f32 + 8.0),
164    );
165}
166
167/// Display de monedas/oro
168pub fn draw_gold(
169    gui: &mut migui::Migui,
170    x: f32,
171    y: f32,
172    gold: i64,
173    theme: &Theme,
174) {
175    let label = format!("🪙 {}", gold);
176    gui.label(
177        migui::WidgetId::new("gold"),
178        &label,
179        migui::Rect::new(x, y, 150.0, theme.font_size as f32 + 8.0),
180    );
181}
182
183/// Display de tiempo
184pub fn draw_timer(
185    gui: &mut migui::Migui,
186    x: f32,
187    y: f32,
188    seconds: f32,
189    theme: &Theme,
190) {
191    let mins = (seconds / 60.0) as i32;
192    let secs = (seconds % 60.0) as i32;
193    let label = format!("{:02}:{:02}", mins, secs);
194    gui.label(
195        migui::WidgetId::new("timer"),
196        &label,
197        migui::Rect::new(x, y, 80.0, theme.font_size as f32 + 8.0),
198    );
199}
200
201/// HUD completo (vida + maná + score)
202pub fn draw_full_hud(
203    gui: &mut migui::Migui,
204    hp: f32,
205    max_hp: f32,
206    mp: f32,
207    max_mp: f32,
208    xp: f32,
209    max_xp: f32,
210    score: i64,
211    theme: &Theme,
212) {
213    let bar_w = 200.0;
214    let bar_h = 18.0;
215    let x = 20.0;
216    let mut y = 20.0;
217
218    draw_health_bar(gui, x, y, bar_w, bar_h, hp, max_hp, theme);
219    y += bar_h + 4.0;
220
221    draw_mana_bar(gui, x, y, bar_w, bar_h, mp, max_mp, theme);
222    y += bar_h + 4.0;
223
224    draw_xp_bar(gui, x, y, bar_w, bar_h, xp, max_xp, theme);
225
226    draw_score(gui, 600.0, 20.0, score, theme);
227}
228
229// ============================================================================
230// MENU WIDGETS
231// ============================================================================
232
233/// Menú principal — retorna índice de opción seleccionada (-1 = ninguna)
234pub fn draw_main_menu(
235    gui: &mut migui::Migui,
236    title: &str,
237    options: &[&str],
238    theme: &Theme,
239    hover_state: &mut i32,
240) -> i32 {
241    let screen_w = 800.0;
242    let screen_h = 600.0;
243
244    // Fondo semi-transparente
245    gui.panel(
246        migui::WidgetId::new("menu_bg"),
247        migui::Rect::new(0.0, 0.0, screen_w, screen_h),
248        rgba_to_migui(theme.menu_bg),
249    );
250
251    // Título
252    gui.label(
253        migui::WidgetId::new("menu_title"),
254        title,
255        migui::Rect::new(
256            screen_w / 2.0 - 150.0,
257            80.0,
258            300.0,
259            theme.font_size_title as f32 + 10.0,
260        ),
261    );
262
263    // Opciones
264    let item_h = 40.0;
265    let total_h = options.len() as f32 * (item_h + theme.spacing);
266    let mut y = (screen_h - total_h) / 2.0;
267
268    for (i, option) in options.iter().enumerate() {
269        let id = format!("menu_opt_{}", i);
270        let is_hovered = *hover_state == i as i32;
271
272        let bg = if is_hovered {
273            rgba_to_migui(theme.menu_item_hover)
274        } else {
275            rgba_to_migui(theme.menu_item_bg)
276        };
277
278        gui.panel(
279            migui::WidgetId::new(&format!("{}_bg", id)),
280            migui::Rect::new(screen_w / 2.0 - 120.0, y, 240.0, item_h),
281            bg,
282        );
283
284        if gui.button(
285            migui::WidgetId::new(&id),
286            migui::Rect::new(screen_w / 2.0 - 110.0, y + 4.0, 220.0, item_h - 8.0),
287            option,
288        ) {
289            return i as i32;
290        }
291
292        y += item_h + theme.spacing;
293    }
294
295    -1
296}
297
298/// Menú de pausa — retorna opción seleccionada
299pub fn draw_pause_menu(
300    gui: &mut migui::Migui,
301    theme: &Theme,
302    hover_state: &mut i32,
303) -> i32 {
304    draw_main_menu(
305        gui,
306        "PAUSA",
307        &["Continuar", "Opciones", "Guardar", "Salir"],
308        theme,
309        hover_state,
310    )
311}
312
313/// Game Over screen
314pub fn draw_game_over(
315    gui: &mut migui::Migui,
316    score: i64,
317    theme: &Theme,
318    hover_state: &mut i32,
319) -> i32 {
320    draw_main_menu(
321        gui,
322        &format!("GAME OVER\nScore: {}", score),
323        &["Reiniciar", "Menú Principal"],
324        theme,
325        hover_state,
326    )
327}
328
329/// Menú de opciones
330pub fn draw_options_menu(
331    gui: &mut migui::Migui,
332    theme: &Theme,
333    volume: &mut f32,
334    fullscreen: &mut bool,
335    hover_state: &mut i32,
336) {
337    let screen_w = 800.0;
338    let screen_h = 600.0;
339
340    gui.panel(
341        migui::WidgetId::new("options_bg"),
342        migui::Rect::new(0.0, 0.0, screen_w, screen_h),
343        rgba_to_migui(theme.menu_bg),
344    );
345
346    gui.label(
347        migui::WidgetId::new("options_title"),
348        "OPCIONES",
349        migui::Rect::new(
350            screen_w / 2.0 - 100.0,
351            40.0,
352            200.0,
353            theme.font_size_title as f32 + 10.0,
354        ),
355    );
356
357    // Volumen
358    gui.label(
359        migui::WidgetId::new("vol_label"),
360        &format!("Volumen: {:.0}%", *volume * 100.0),
361        migui::Rect::new(200.0, 150.0, 200.0, 30.0),
362    );
363
364    *volume = gui.slider(
365        migui::WidgetId::new("vol_slider"),
366        *volume,
367        0.0,
368        1.0,
369        migui::Rect::new(420.0, 150.0, 200.0, 30.0),
370    );
371
372    // Fullscreen
373    gui.checkbox(
374        migui::WidgetId::new("fs_check"),
375        "Pantalla Completa",
376        fullscreen,
377        migui::Rect::new(200.0, 200.0, 300.0, 30.0),
378    );
379
380    // Botón volver
381    if gui.button(
382        migui::WidgetId::new("options_back"),
383        migui::Rect::new(screen_w / 2.0 - 60.0, 400.0, 120.0, 40.0),
384        "Volver",
385    ) {
386        *hover_state = 99;
387    }
388}
389
390// ============================================================================
391/// Diálogo de NPC
392pub fn draw_dialog(
393    gui: &mut migui::Migui,
394    npc_name: &str,
395    text: &str,
396    x: f32,
397    y: f32,
398    width: f32,
399    height: f32,
400    theme: &Theme,
401) {
402    // Fondo del diálogo
403    gui.panel(
404        migui::WidgetId::new("dialog_bg"),
405        migui::Rect::new(x, y, width, height),
406        rgba_to_migui(theme.dialog_bg),
407    );
408
409    // Borde
410    if theme.border_width > 0.0 {
411        gui.panel(
412            migui::WidgetId::new("dialog_border"),
413            migui::Rect::new(x, y, width, height),
414            rgba_to_migui(ColorRGBA::transparent()),
415        );
416    }
417
418    // Nombre del NPC
419    gui.label(
420        migui::WidgetId::new("dialog_name"),
421        npc_name,
422        migui::Rect::new(x + 10.0, y + 4.0, width - 20.0, theme.font_size_small as f32 + 6.0),
423    );
424
425    // Texto del diálogo
426    gui.label(
427        migui::WidgetId::new("dialog_text"),
428        text,
429        migui::Rect::new(
430            x + 10.0,
431            y + theme.font_size_small as f32 + 12.0,
432            width - 20.0,
433            height - theme.font_size_small as f32 - 20.0,
434        ),
435    );
436}
437
438/// Message box (confirmación)
439pub fn draw_message_box(
440    gui: &mut migui::Migui,
441    title: &str,
442    message: &str,
443    buttons: &[&str],
444    theme: &Theme,
445) -> i32 {
446    let screen_w = 800.0;
447    let screen_h = 600.0;
448    let box_w = 400.0;
449    let box_h = 200.0;
450    let x = (screen_w - box_w) / 2.0;
451    let y = (screen_h - box_h) / 2.0;
452
453    // Fondo
454    gui.panel(
455        migui::WidgetId::new("msg_bg"),
456        migui::Rect::new(0.0, 0.0, screen_w, screen_h),
457        rgba_to_migui(ColorRGBA { r: 0, g: 0, b: 0, a: 128 }),
458    );
459
460    gui.panel(
461        migui::WidgetId::new("msg_box"),
462        migui::Rect::new(x, y, box_w, box_h),
463        rgba_to_migui(theme.dialog_bg),
464    );
465
466    // Título
467    gui.label(
468        migui::WidgetId::new("msg_title"),
469        title,
470        migui::Rect::new(x + 10.0, y + 8.0, box_w - 20.0, theme.font_size as f32 + 6.0),
471    );
472
473    // Mensaje
474    gui.label(
475        migui::WidgetId::new("msg_text"),
476        message,
477        migui::Rect::new(x + 10.0, y + 40.0, box_w - 20.0, 80.0),
478    );
479
480    // Botones
481    let btn_w = 100.0;
482    let btn_h = 35.0;
483    let total_w = buttons.len() as f32 * (btn_w + 10.0);
484    let mut btn_x = x + (box_w - total_w) / 2.0;
485    let btn_y = y + box_h - 50.0;
486
487    for (i, label) in buttons.iter().enumerate() {
488        if gui.button(
489            migui::WidgetId::new(&format!("msg_btn_{}", i)),
490            migui::Rect::new(btn_x, btn_y, btn_w, btn_h),
491            label,
492        ) {
493            return i as i32;
494        }
495        btn_x += btn_w + 10.0;
496    }
497
498    -1
499}
500
501// ============================================================================
502// INVENTARIO
503// ============================================================================
504
505/// Slot de inventario individual
506pub fn draw_inventory_slot(
507    gui: &mut migui::Migui,
508    x: f32,
509    y: f32,
510    size: f32,
511    item_name: Option<&str>,
512    count: i32,
513    is_selected: bool,
514    theme: &Theme,
515) {
516    let bg = if is_selected {
517        rgba_to_migui(theme.slot_hover)
518    } else {
519        rgba_to_migui(theme.slot_bg)
520    };
521
522    gui.panel(
523        migui::WidgetId::new(&format!("slot_{}_{}", x as i32, y as i32)),
524        migui::Rect::new(x, y, size, size),
525        bg,
526    );
527
528    if let Some(name) = item_name {
529        gui.label(
530            migui::WidgetId::new(&format!("slot_label_{}_{}", x as i32, y as i32)),
531            name,
532            migui::Rect::new(x + 2.0, y + 2.0, size - 4.0, size - 14.0),
533        );
534
535        if count > 1 {
536            gui.label(
537                migui::WidgetId::new(&format!("slot_count_{}_{}", x as i32, y as i32)),
538                &format!("x{}", count),
539                migui::Rect::new(
540                    x + size - 30.0,
541                    y + size - 14.0,
542                    28.0,
543                    theme.font_size_small as f32,
544                ),
545            );
546        }
547    }
548}
549
550/// Grid de inventario completo
551pub fn draw_inventory_grid(
552    gui: &mut migui::Migui,
553    x: f32,
554    y: f32,
555    cols: usize,
556    rows: usize,
557    slot_size: f32,
558    spacing: f32,
559    items: &[Option<(String, i32)>],
560    selected_slot: &mut usize,
561    theme: &Theme,
562) -> Option<usize> {
563    // Fondo del panel de inventario
564    let panel_w = cols as f32 * (slot_size + spacing) + theme.padding * 2.0;
565    let panel_h = rows as f32 * (slot_size + spacing) + theme.padding * 2.0 + 30.0;
566
567    gui.panel(
568        migui::WidgetId::new("inv_panel"),
569        migui::Rect::new(x, y, panel_w, panel_h),
570        rgba_to_migui(theme.dialog_bg),
571    );
572
573    gui.label(
574        migui::WidgetId::new("inv_title"),
575        "INVENTARIO",
576        migui::Rect::new(
577            x + theme.padding,
578            y + 4.0,
579            panel_w - theme.padding * 2.0,
580            theme.font_size as f32 + 6.0,
581        ),
582    );
583
584    let slot_y = y + theme.padding + 30.0;
585
586    for row in 0..rows {
587        for col in 0..cols {
588            let idx = row * cols + col;
589            let slot_x = x + theme.padding + col as f32 * (slot_size + spacing);
590            let slot_y = slot_y + row as f32 * (slot_size + spacing);
591
592            let item = items.get(idx).and_then(|i| i.as_ref());
593            let (item_name, count) = match item {
594                Some((name, c)) => (Some(name.as_str()), *c),
595                None => (None, 0),
596            };
597
598            let is_selected = idx == *selected_slot;
599
600            if gui.button(
601                migui::WidgetId::new(&format!("inv_slot_{}", idx)),
602                migui::Rect::new(slot_x, slot_y, slot_size, slot_size),
603                "",
604            ) {
605                *selected_slot = idx;
606                return Some(idx);
607            }
608
609            draw_inventory_slot(
610                gui,
611                slot_x,
612                slot_y,
613                slot_size,
614                item_name,
615                count,
616                is_selected,
617                theme,
618            );
619        }
620    }
621
622    None
623}
624
625// ============================================================================
626// NOTIFICACIONES
627// ============================================================================
628
629/// Notificación temporal (toast)
630pub fn draw_notification(
631    gui: &mut migui::Migui,
632    text: &str,
633    x: f32,
634    y: f32,
635    theme: &Theme,
636) {
637    let text_w = text.len() as f32 * theme.font_size as f32 * 0.5;
638    let h = theme.font_size as f32 + 16.0;
639
640    gui.panel(
641        migui::WidgetId::new("notif_bg"),
642        migui::Rect::new(x, y, text_w + 20.0, h),
643        rgba_to_migui(theme.menu_item_hover),
644    );
645
646    gui.label(
647        migui::WidgetId::new("notif_text"),
648        text,
649        migui::Rect::new(x + 10.0, y + 8.0, text_w, theme.font_size as f32),
650    );
651}
652
653// ============================================================================
654// MINIMAP
655// ============================================================================
656
657/// Minimapa simple
658pub fn draw_minimap(
659    gui: &mut migui::Migui,
660    x: f32,
661    y: f32,
662    size: f32,
663    player_px: f32,
664    player_py: f32,
665    world_w: f32,
666    world_h: f32,
667    theme: &Theme,
668) {
669    // Fondo del minimap
670    gui.panel(
671        migui::WidgetId::new("minimap_bg"),
672        migui::Rect::new(x, y, size, size),
673        rgba_to_migui(theme.slot_bg),
674    );
675
676    // Jugador (punto)
677    let dot_x = x + (player_px / world_w) * size;
678    let dot_y = y + (player_py / world_h) * size;
679    let dot_size = 6.0;
680
681    gui.panel(
682        migui::WidgetId::new("minimap_player"),
683        migui::Rect::new(dot_x - dot_size / 2.0, dot_y - dot_size / 2.0, dot_size, dot_size),
684        rgba_to_migui(theme.health_bar_fill),
685    );
686}
687
688// ============================================================================
689// LOADING SCREEN
690// ============================================================================
691
692/// Pantalla de carga
693pub fn draw_loading(
694    gui: &mut migui::Migui,
695    text: &str,
696    progress: f32,
697    theme: &Theme,
698) {
699    let screen_w = 800.0;
700    let screen_h = 600.0;
701
702    gui.panel(
703        migui::WidgetId::new("load_bg"),
704        migui::Rect::new(0.0, 0.0, screen_w, screen_h),
705        rgba_to_migui(theme.bg_color),
706    );
707
708    gui.label(
709        migui::WidgetId::new("load_text"),
710        text,
711        migui::Rect::new(
712            screen_w / 2.0 - 100.0,
713            250.0,
714            200.0,
715            theme.font_size as f32 + 8.0,
716        ),
717    );
718
719    let bar_w = 300.0;
720    let bar_h = 20.0;
721    let bar_x = (screen_w - bar_w) / 2.0;
722    let bar_y = 300.0;
723
724    gui.panel(
725        migui::WidgetId::new("load_bar_bg"),
726        migui::Rect::new(bar_x, bar_y, bar_w, bar_h),
727        rgba_to_migui(theme.slot_bg),
728    );
729
730    let fill_w = bar_w * progress.clamp(0.0, 1.0);
731    gui.panel(
732        migui::WidgetId::new("load_bar_fill"),
733        migui::Rect::new(bar_x, bar_y, fill_w, bar_h),
734        rgba_to_migui(theme.health_bar_fill),
735    );
736
737    let pct = format!("{:.0}%", progress * 100.0);
738    gui.label(
739        migui::WidgetId::new("load_pct"),
740        &pct,
741        migui::Rect::new(
742            screen_w / 2.0 - 20.0,
743            bar_y + bar_h + 8.0,
744            40.0,
745            theme.font_size_small as f32,
746        ),
747    );
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    #[test]
755    fn test_color_conversion() {
756        let c = ColorRGBA::rgb(255, 128, 64);
757        let migui_c = rgba_to_migui(c);
758        assert_eq!(migui_c.r, 255);
759        assert_eq!(migui_c.g, 128);
760        assert_eq!(migui_c.b, 64);
761    }
762
763    #[test]
764    fn test_sdl2_color_conversion() {
765        let c = ColorRGBA::rgb(100, 150, 200);
766        let sdl2_c = rgba_to_sdl2(c);
767        assert_eq!(sdl2_c.r, 100);
768        assert_eq!(sdl2_c.g, 150);
769        assert_eq!(sdl2_c.b, 200);
770    }
771
772    #[test]
773    fn test_rgba_transparent() {
774        let c = ColorRGBA::transparent();
775        assert_eq!(c.a, 0);
776    }
777}