rosu_renderer/layout/
mania.rs

1use egui::{self, pos2, Color32, Rect, Vec2};
2use rosu_map::section::hit_objects::{HitObject, HitObjectKind};
3
4#[derive(Clone)]
5pub enum NoteShape {
6    Circle,
7    Rectangle { width: f32, height: f32 },
8    Arrow { width: f32, height: f32 },
9    Image(egui::Image<'static>),
10}
11
12pub struct NoteStyle {
13    pub shape: NoteShape,
14    pub color: Color32,
15    pub hold_body_color: Color32,
16    pub hold_cap_color: Color32,
17}
18
19impl Default for NoteStyle {
20    fn default() -> Self {
21        Self {
22            shape: NoteShape::Rectangle {
23                width: 0.8,
24                height: 0.25,
25            }, // Rectangle par défaut
26            color: Color32::from_rgb(0, 174, 255),
27            hold_body_color: Color32::from_rgb(200, 200, 200),
28            hold_cap_color: Color32::from_rgb(0, 174, 255),
29        }
30    }
31}
32
33pub struct ManiaRenderer {
34    column_width: f32,
35    note_size: f32,
36    speed: f64,
37    height: f32,
38    note_style: NoteStyle,
39}
40
41impl ManiaRenderer {
42    pub fn with_sizes(column_width: f32, note_size: f32, height: f32) -> Self {
43        Self {
44            column_width,
45            note_size,
46            speed: 1.0,
47            height,
48            note_style: NoteStyle::default(),
49        }
50    }
51
52    pub fn set_note_style(&mut self, style: NoteStyle) {
53        self.note_style = style;
54    }
55
56    fn draw_note(&self, ui: &mut egui::Ui, x_pos: f32, y_pos: f32) {
57        let center_x = x_pos + self.column_width / 2.0;
58
59        match &self.note_style.shape {
60            NoteShape::Circle => {
61                let circle_radius = self.note_size / 2.0;
62                ui.painter().circle_filled(
63                    pos2(center_x, y_pos),
64                    circle_radius,
65                    self.note_style.color,
66                );
67            }
68            NoteShape::Rectangle { width, height } => {
69                let note_width = self.note_size * width;
70                let note_height = self.note_size * height;
71                let rect = Rect::from_center_size(
72                    pos2(center_x, y_pos),
73                    Vec2::new(note_width, note_height),
74                );
75                ui.painter().rect_filled(rect, 0.0, self.note_style.color);
76            }
77            NoteShape::Arrow { width, height } => {
78                let note_width = self.note_size * width;
79                let note_height = self.note_size * height;
80                let points = vec![
81                    pos2(center_x, y_pos - note_height / 2.0), // Pointe
82                    pos2(center_x + note_width / 2.0, y_pos + note_height / 2.0), // Droite
83                    pos2(center_x - note_width / 2.0, y_pos + note_height / 2.0), // Gauche
84                ];
85                ui.painter().add(egui::Shape::convex_polygon(
86                    points,
87                    self.note_style.color,
88                    egui::Stroke::NONE,
89                ));
90            }
91            NoteShape::Image(image) => {
92                image.paint_at(
93                    ui,
94                    Rect::from_min_size(
95                        pos2(
96                            center_x - self.note_size / 2.0,
97                            y_pos - self.note_size / 2.0,
98                        ),
99                        Vec2::new(self.note_size, self.note_size),
100                    ),
101                );
102            }
103        }
104    }
105
106    fn render_hold(
107        &self,
108        ui: &mut egui::Ui,
109        x_pos: f32,
110        start_y: f32,
111        end_y: f32,
112        judgment_line_y: f32,
113    ) {
114        let note_width = self.note_size * 0.8;
115        let x_center = x_pos + (self.column_width - note_width) / 2.0;
116
117        let y_start = start_y.min(end_y);
118        let y_end = (start_y.max(end_y)).min(judgment_line_y);
119        let visible_height = (y_end - y_start).abs();
120
121        // Hold body
122        ui.painter().rect_filled(
123            Rect::from_min_size(
124                pos2(x_center, y_start),
125                Vec2::new(note_width, visible_height),
126            ),
127            0.0,
128            self.note_style.hold_body_color,
129        );
130
131        // Hold end cap
132        let cap_height = note_width * 0.3;
133        if end_y <= judgment_line_y {
134            ui.painter().rect_filled(
135                Rect::from_min_size(pos2(x_center, end_y), Vec2::new(note_width, cap_height)),
136                0.0,
137                self.note_style.hold_cap_color,
138            );
139        }
140    }
141
142    pub fn set_height(&mut self, height: f32) {
143        self.height = height;
144    }
145
146    pub fn required_width(&self, keycount: usize) -> f32 {
147        self.column_width * keycount as f32
148    }
149
150    pub fn required_height(&self) -> f32 {
151        self.height
152    }
153
154    pub fn render(
155        &mut self,
156        ui: &mut egui::Ui,
157        hit_objects: &[HitObject],
158        current_time: f64,
159        scroll_time_ms: f32,
160        speed: f64,
161        keycount: usize,
162    ) {
163        self.render_at(ui, hit_objects, current_time, scroll_time_ms, speed, keycount, pos2(0.0, 0.0))
164    }
165
166    pub fn render_at(
167        &mut self,
168        ui: &mut egui::Ui,
169        hit_objects: &[HitObject],
170        current_time: f64,
171        scroll_time_ms: f32,
172        speed: f64,
173        keycount: usize,
174        position: egui::Pos2,
175    ) {
176        self.speed = speed;
177
178        let total_width = self.required_width(keycount);
179        let total_height = self.required_height();
180
181        // Draw background rectangle at the specified position
182        let background_rect = egui::Rect::from_min_size(position, egui::Vec2::new(total_width, total_height));
183        ui.painter().rect_filled(background_rect, 0.0, egui::Color32::from_gray(20));
184
185        // Draw columns
186        for i in 0..keycount {
187            let column_rect = egui::Rect::from_min_size(
188                egui::pos2(position.x + i as f32 * self.column_width, position.y),
189                egui::Vec2::new(self.column_width, total_height),
190            );
191            ui.painter()
192                .rect_filled(column_rect, 0.0, egui::Color32::from_gray(30));
193        }
194
195        let judgment_line_y = position.y + total_height - 100.0;
196        ui.painter().line_segment(
197            [
198                egui::pos2(position.x, judgment_line_y),
199                egui::pos2(position.x + total_width, judgment_line_y),
200            ],
201            egui::Stroke::new(2.0, egui::Color32::WHITE),
202        );
203
204        // Only process hit objects if there are any
205        if !hit_objects.is_empty() {
206            // Pre-calculate time bounds for visible objects
207            let visible_start_time = current_time - scroll_time_ms as f64 * 2.0;
208            let visible_end_time = current_time + scroll_time_ms as f64 * 0.5;
209            
210            // Draw hold notes first
211            for hit_object in hit_objects
212                .iter()
213                .filter(|h| {
214                    let obj_time = h.start_time / speed;
215                    // For hold notes, also check the end time
216                    let obj_end_time = if let HitObjectKind::Hold(hold) = &h.kind {
217                        (h.start_time + hold.duration) / speed
218                    } else {
219                        obj_time
220                    };
221                    // Show if either start or end is visible
222                    (obj_time >= visible_start_time && obj_time <= visible_end_time) ||
223                    (obj_end_time >= visible_start_time && obj_end_time <= visible_end_time) ||
224                    (obj_time <= visible_start_time && obj_end_time >= visible_end_time) // Note spans the visible area
225                })
226                .filter(|h| matches!(h.kind, HitObjectKind::Hold(_)))
227            {
228                if let HitObjectKind::Hold(h) = &hit_object.kind {
229                    let column = (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
230                    let x_pos = position.x + column as f32 * self.column_width;
231
232                    let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
233                    let end_time =
234                        (hit_object.start_time + h.duration) / speed + scroll_time_ms as f64;
235
236                    let time_diff = note_time - current_time;
237                    let end_time_diff = end_time - current_time;
238
239                    let y_pos =
240                        judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
241                    let end_y_pos = judgment_line_y
242                        - (end_time_diff as f32 / scroll_time_ms) * total_height;
243
244                    if end_y_pos <= judgment_line_y {
245                        self.render_hold(ui, x_pos, y_pos, end_y_pos, judgment_line_y);
246                    }
247                }
248            }
249
250            // Then draw regular notes
251            for hit_object in hit_objects
252                .iter()
253                .filter(|h| {
254                    let obj_time = h.start_time / speed;
255                    obj_time >= visible_start_time && obj_time <= visible_end_time
256                })
257            {
258                let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
259                let time_diff = note_time - current_time;
260                let y_pos =
261                    judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
262
263                if y_pos <= judgment_line_y {
264                    let x_pos = match &hit_object.kind {
265                        HitObjectKind::Circle(h) => {
266                            let column =
267                                (h.pos.x / 512.0 * keycount as f32) as usize % keycount;
268                            position.x + column as f32 * self.column_width
269                        }
270                        HitObjectKind::Hold(h) => {
271                            let column =
272                                (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
273                            position.x + column as f32 * self.column_width
274                        }
275                        _ => continue,
276                    };
277
278                    // Draw notes when they start entering the screen (considering note height)
279                    let note_height = self.note_size * 0.25; // Approximate note height
280                    if y_pos >= -note_height {
281                        self.draw_note(ui, x_pos, y_pos);
282                    }
283                }
284            }
285        }
286    }
287}