Skip to main content

rustapi/docks/
tiles.rs

1use crate::prelude::*;
2use rusterix::{TileRole, VertexBlendPreset};
3
4pub struct TilesDock {
5    pub tile_ids: FxHashMap<(i32, i32), Uuid>,
6
7    pub filter: String,
8    pub filter_role: u8,
9    pub zoom: f32,
10
11    pub curr_tile: Option<Uuid>,
12
13    pub tile_preview_mode: bool,
14    pub tile_hover_id: Uuid,
15
16    blend_index: usize,
17}
18
19impl Dock for TilesDock {
20    fn new() -> Self
21    where
22        Self: Sized,
23    {
24        Self {
25            tile_ids: FxHashMap::default(),
26            filter: "".to_string(),
27            filter_role: 0,
28            zoom: 1.5,
29            curr_tile: None,
30
31            tile_preview_mode: false,
32            tile_hover_id: Uuid::nil(),
33
34            blend_index: 0,
35        }
36    }
37
38    fn setup(&mut self, _ctx: &mut TheContext) -> TheCanvas {
39        let mut canvas = TheCanvas::new();
40
41        // Toolbar
42        let mut toolbar_canvas = TheCanvas::default();
43        let traybar_widget = TheTraybar::new(TheId::empty());
44        toolbar_canvas.set_widget(traybar_widget);
45        let mut toolbar_hlayout = TheHLayout::new(TheId::empty());
46        toolbar_hlayout.set_background_color(None);
47
48        let mut filter_text = TheText::new(TheId::empty());
49        filter_text.set_text(fl!("filter"));
50
51        toolbar_hlayout.set_margin(Vec4::new(10, 1, 5, 1));
52        toolbar_hlayout.set_padding(3);
53        toolbar_hlayout.add_widget(Box::new(filter_text));
54        let mut filter_edit = TheTextLineEdit::new(TheId::named("Tiles Dock Filter Edit"));
55        filter_edit.set_text("".to_string());
56        filter_edit.limiter_mut().set_max_size(Vec2::new(120, 18));
57        filter_edit.set_font_size(12.5);
58        // filter_edit.set_embedded(true);
59        filter_edit.set_status_text(&fl!("status_tiles_filter_edit"));
60        filter_edit.set_continuous(true);
61        toolbar_hlayout.add_widget(Box::new(filter_edit));
62
63        let mut drop_down = TheDropdownMenu::new(TheId::named("Tiles Dock Filter Role"));
64        drop_down.add_option(fl!("all"));
65        for dir in TileRole::iterator() {
66            drop_down.add_option(dir.to_string().to_string());
67        }
68        toolbar_hlayout.add_widget(Box::new(drop_down));
69
70        let mut spacer = TheSpacer::new(TheId::empty());
71        spacer.limiter_mut().set_max_width(10);
72        toolbar_hlayout.add_widget(Box::new(spacer));
73
74        let mut zoom = TheSlider::new(TheId::named("Tiles Dock Zoom"));
75        zoom.set_value(TheValue::Float(self.zoom));
76        zoom.set_default_value(TheValue::Float(1.5));
77        zoom.set_range(TheValue::RangeF32(1.0..=3.0));
78        zoom.set_continuous(true);
79        zoom.limiter_mut().set_max_width(120);
80        toolbar_hlayout.add_widget(Box::new(zoom));
81        toolbar_hlayout.set_reverse_index(Some(1));
82
83        toolbar_canvas.set_layout(toolbar_hlayout);
84        canvas.set_top(toolbar_canvas);
85
86        let mut rgba_layout = TheRGBALayout::new(TheId::named("Tiles Dock RGBA Layout"));
87        if let Some(rgba_view) = rgba_layout.rgba_view_mut().as_rgba_view() {
88            rgba_view.set_supports_external_zoom(true);
89            rgba_view.set_background([116, 116, 116, 255]);
90            rgba_view.set_grid(Some(24));
91            rgba_view.set_mode(TheRGBAViewMode::TilePicker);
92            let mut c = WHITE;
93            c[3] = 128;
94            rgba_view.set_hover_color(Some(c));
95        }
96
97        // Bottom toolbar
98        let mut toolbar_canvas = TheCanvas::default();
99        let traybar_widget = TheTraybar::new(TheId::empty());
100        toolbar_canvas.set_widget(traybar_widget);
101        let mut toolbar_hlayout = TheHLayout::new(TheId::empty());
102        toolbar_hlayout.set_background_color(None);
103
104        toolbar_hlayout.set_margin(Vec4::new(10, 1, 5, 1));
105        toolbar_hlayout.set_padding(3);
106
107        let size = 24;
108        for (index, p) in VertexBlendPreset::ALL.iter().enumerate() {
109            let weights = p.weights();
110            let buffer = p.preview_vertex_blend(weights, size);
111            let rgba = TheRGBABuffer::from(buffer, size as u32, size as u32);
112            let mut view = TheIconView::new(TheId::named(&format!("Blend #{}", index)));
113            view.set_rgba_tile(TheRGBATile::buffer(rgba));
114            if index == 0 {
115                view.set_border_color(Some(WHITE));
116            }
117            toolbar_hlayout.add_widget(Box::new(view));
118
119            if index == 2 || index == 6 || index == 10 || index == 14 {
120                let mut spacer = TheSpacer::new(TheId::empty());
121                spacer.limiter_mut().set_max_width(4);
122                toolbar_hlayout.add_widget(Box::new(spacer));
123            }
124        }
125
126        toolbar_canvas.set_layout(toolbar_hlayout);
127        canvas.set_bottom(toolbar_canvas);
128
129        // ---
130
131        canvas.set_layout(rgba_layout);
132
133        canvas
134    }
135
136    fn activate(
137        &mut self,
138        ui: &mut TheUI,
139        ctx: &mut TheContext,
140        project: &Project,
141        _server_ctx: &mut ServerContext,
142    ) {
143        self.set_tiles(&project.tiles, ui, ctx);
144    }
145
146    fn handle_event(
147        &mut self,
148        event: &TheEvent,
149        ui: &mut TheUI,
150        ctx: &mut TheContext,
151        project: &mut Project,
152        server_ctx: &mut ServerContext,
153    ) -> bool {
154        if server_ctx.help_mode {
155            let open_tiles_help = match event {
156                TheEvent::TilePicked(id, _) => id.name == "Tiles Dock RGBA Layout View",
157                TheEvent::StateChanged(id, state) if *state == TheWidgetState::Clicked => {
158                    id.name == "Tiles"
159                        || id.name == "Tiles Dock RGBA Layout View"
160                        || id.name.starts_with("Blend #")
161                }
162                TheEvent::MouseDown(coord) => ui
163                    .get_widget_at_coord(*coord)
164                    .map(|w| {
165                        let name = &w.id().name;
166                        name == "Tiles Dock RGBA Layout View"
167                            || name == "Tiles"
168                            || name.starts_with("Blend #")
169                    })
170                    .unwrap_or(false),
171                _ => false,
172            };
173            if open_tiles_help {
174                ctx.ui.send(TheEvent::Custom(
175                    TheId::named("Show Help"),
176                    TheValue::Text("docs/creator/docks/tile_picker_editor".into()),
177                ));
178                return true;
179            }
180        }
181
182        let mut redraw = false;
183
184        match event {
185            TheEvent::WidgetResized(id, _) => {
186                if id.name == "Tiles Dock RGBA Layout View" {
187                    self.set_tiles(&project.tiles, ui, ctx);
188                }
189            }
190            TheEvent::StateChanged(id, TheWidgetState::Clicked) => {
191                if id.name.starts_with("Blend #") {
192                    if let Ok(index) = id.name.strip_prefix("Blend #").unwrap().parse::<usize>() {
193                        if let Some(old_icon) =
194                            ui.get_icon_view(&format!("Blend #{}", self.blend_index))
195                        {
196                            old_icon.set_border_color(None);
197                        }
198                        if let Some(old_icon) = ui.get_icon_view(&format!("Blend #{}", index)) {
199                            old_icon.set_border_color(Some(WHITE));
200                        }
201                        self.blend_index = index;
202                        server_ctx.rect_blend_preset = VertexBlendPreset::from_index(index)
203                            .unwrap_or(VertexBlendPreset::Solid);
204                    }
205                } else if id.name == "Tiles Dock Tile Copy" {
206                    if let Some(tile_id) = self.curr_tile {
207                        let txt = format!("\"{tile_id}\"");
208                        ctx.ui.clipboard = Some(TheValue::Text(txt.clone()));
209                        let mut clipboard = arboard::Clipboard::new().unwrap();
210                        clipboard.set_text(txt.clone()).unwrap();
211                    }
212                }
213            }
214            TheEvent::Resize => {
215                self.set_tiles(&project.tiles, ui, ctx);
216            }
217            TheEvent::TileDragStarted(id, pos, _offset) => {
218                if id.name == "Tiles Dock RGBA Layout View" {
219                    if let Some(tile_id) = self.tile_ids.get(&(pos.x, pos.y)) {
220                        if let Some(tile) = project.tiles.get(tile_id) {
221                            let mut drop = TheDrop::new(TheId::named_with_id("Tile", *tile_id));
222                            if !tile.is_empty() {
223                                let b = TheRGBABuffer::from(
224                                    tile.textures[0].data.clone(),
225                                    tile.textures[0].width as u32,
226                                    tile.textures[0].height as u32,
227                                );
228                                drop.set_image(b.scaled(
229                                    (tile.textures[0].width as f32 * self.zoom) as i32,
230                                    (tile.textures[0].height as f32 * self.zoom) as i32,
231                                ));
232                            }
233                            ctx.ui.set_drop(drop);
234                        }
235                    }
236                }
237            }
238            TheEvent::TilePicked(id, pos) => {
239                if id.name == "Tiles Dock RGBA Layout View" {
240                    if let Some(tile_id) = self.tile_ids.get(&(pos.x, pos.y)) {
241                        server_ctx.curr_tile_id = Some(*tile_id);
242                        ctx.ui.send(TheEvent::Custom(
243                            TheId::named("Tile Picked"),
244                            TheValue::Id(*tile_id),
245                        ));
246                        ctx.ui.send(TheEvent::Custom(
247                            TheId::named("Update Action List"),
248                            TheValue::Empty,
249                        ));
250                        self.curr_tile = Some(*tile_id);
251                        redraw = true;
252                    }
253                }
254            }
255            TheEvent::LostHover(id) => {
256                if id.name == "Tiles Dock RGBA Layout View" {
257                    self.tile_preview_mode = false;
258                    ctx.ui.send(TheEvent::Custom(
259                        TheId::named("Soft Update Minimap"),
260                        TheValue::Empty,
261                    ));
262                }
263            }
264            TheEvent::TileEditorHoverChanged(id, pos) => {
265                if id.name == "Tiles Dock RGBA Layout View" {
266                    if let Some(tile_id) = self.tile_ids.get(&(pos.x, pos.y)) {
267                        if let Some(tile) = project.get_tile(tile_id) {
268                            if tile.name.is_empty() {
269                                let text = format!(
270                                    "{}, Blocking: {}",
271                                    tile.role.to_string(),
272                                    if tile.blocking { "Yes" } else { "No" },
273                                );
274                                ctx.ui.send(TheEvent::SetStatusText(id.clone(), text));
275                            } else {
276                                let text = format!(
277                                    "{}, Blocking: {}, Tags: \"{}\"",
278                                    tile.role.to_string(),
279                                    if tile.blocking { "Yes" } else { "No" },
280                                    tile.name
281                                );
282                                ctx.ui.send(TheEvent::SetStatusText(id.clone(), text));
283                            }
284                        }
285
286                        self.tile_preview_mode = true;
287                        self.tile_hover_id = *tile_id;
288                        ctx.ui.send(TheEvent::Custom(
289                            TheId::named("Soft Update Minimap"),
290                            TheValue::Empty,
291                        ));
292                    }
293                    redraw = true;
294                }
295            }
296            TheEvent::TileEditorDelete(id, selected) => {
297                if id.name == "Tiles Dock RGBA Layout View" {
298                    for tile_pos in selected {
299                        if let Some(tile_id) = self.tile_ids.get(tile_pos) {
300                            project.remove_tile(tile_id);
301                        }
302                    }
303                    self.set_tiles(&project.tiles, ui, ctx);
304                }
305            }
306            TheEvent::Custom(id, _value) => {
307                if id.name == "Update Tilepicker" {
308                    self.set_tiles(&project.tiles, ui, ctx);
309                }
310            }
311            TheEvent::TileZoomBy(id, delta) => {
312                if id.name == "Tiles Dock RGBA Layout View" {
313                    self.zoom += *delta * 0.05;
314                    self.zoom = self.zoom.clamp(1.0, 3.0);
315                    self.set_tiles(&project.tiles, ui, ctx);
316                    ui.set_widget_value("Tiles Dock Zoom", ctx, TheValue::Float(self.zoom));
317                }
318            }
319            TheEvent::ValueChanged(id, value) => {
320                if id.name == "Tiles Dock Tile Role" {
321                    if let Some(tile_id) = self.curr_tile {
322                        if let Some(tile) = project.get_tile_mut(&tile_id) {
323                            if let TheValue::Int(role) = value {
324                                tile.role = TileRole::from_index(*role as u8);
325                            }
326                        }
327                    }
328                } else if id.name == "Tiles Dock Tile Tags" {
329                    if let Some(tile_id) = self.curr_tile {
330                        if let Some(tile) = project.get_tile_mut(&tile_id) {
331                            if let TheValue::Text(tags) = value {
332                                tile.name.clone_from(tags);
333                            }
334                        }
335                    }
336                } else if id.name == "Tiles Dock Tile Scale" {
337                    if let Some(tile_id) = self.curr_tile {
338                        if let Some(tile) = project.get_tile_mut(&tile_id) {
339                            if let Some(value) = value.to_f32() {
340                                tile.scale = value;
341                                ctx.ui.send(TheEvent::Custom(
342                                    TheId::named("Update Tiles"),
343                                    TheValue::Empty,
344                                ));
345                            }
346                        }
347                    }
348                } else if id.name == "Tiles Dock Tile Blocking" {
349                    if let Some(tile_id) = self.curr_tile {
350                        if let Some(tile) = project.get_tile_mut(&tile_id) {
351                            if let TheValue::Int(role) = value {
352                                tile.blocking = *role == 1;
353                                ctx.ui.send(TheEvent::Custom(
354                                    TheId::named("Update Tiles"),
355                                    TheValue::Empty,
356                                ));
357                            }
358                        }
359                    }
360                } else if id.name == "Tiles Dock Filter Edit" {
361                    if let TheValue::Text(filter) = value {
362                        self.filter = filter.to_lowercase();
363                        self.set_tiles(&project.tiles, ui, ctx);
364                    }
365                } else if id.name == "Tiles Dock Filter Role" {
366                    if let TheValue::Int(filter) = value {
367                        self.filter_role = *filter as u8;
368                        self.set_tiles(&project.tiles, ui, ctx);
369                    }
370                } else if id.name == "Tiles Dock Zoom" {
371                    if let TheValue::Float(zoom) = value {
372                        self.zoom = *zoom;
373                        self.set_tiles(&project.tiles, ui, ctx);
374                    }
375                }
376            }
377            _ => {}
378        }
379        redraw
380    }
381
382    fn draw_minimap(
383        &self,
384        buffer: &mut TheRGBABuffer,
385        project: &Project,
386        ctx: &mut TheContext,
387        server_ctx: &ServerContext,
388    ) -> bool {
389        if !self.tile_preview_mode {
390            return false;
391        }
392
393        buffer.fill(BLACK);
394
395        if let Some(tile) = project.tiles.get(&self.tile_hover_id) {
396            let index = server_ctx.animation_counter % tile.textures.len();
397
398            let stride: usize = buffer.stride();
399
400            let src_pixels = &tile.textures[index].data;
401            let src_w = tile.textures[index].width as f32;
402            let src_h = tile.textures[index].height as f32;
403
404            let dim = buffer.dim();
405            let dst_w = dim.width as f32;
406            let dst_h = dim.height as f32;
407
408            // Compute scale
409            let scale = (dst_w / src_w).min(dst_h / src_h);
410
411            // Scaled dimensions
412            let draw_w = src_w * scale;
413            let draw_h = src_h * scale;
414
415            // Center
416            let offset_x = ((dst_w - draw_w) * 0.5).round() as usize;
417            let offset_y = ((dst_h - draw_h) * 0.5).round() as usize;
418
419            let dst_rect = (
420                offset_x,
421                offset_y,
422                draw_w.round() as usize,
423                draw_h.round() as usize,
424            );
425
426            ctx.draw.blend_scale_chunk(
427                buffer.pixels_mut(),
428                &dst_rect,
429                stride,
430                src_pixels,
431                &(src_w as usize, src_h as usize),
432            );
433
434            return true;
435        }
436
437        false
438    }
439
440    fn supports_minimap_animation(&self) -> bool {
441        true
442    }
443}
444
445impl TilesDock {
446    /// Set the tiles for the picker.
447    pub fn set_tiles(
448        &mut self,
449        tiles: &IndexMap<Uuid, rusterix::Tile>,
450        ui: &mut TheUI,
451        ctx: &mut TheContext,
452    ) {
453        self.tile_ids.clear();
454        if let Some(editor) = ui.get_rgba_layout("Tiles Dock RGBA Layout") {
455            let width = editor.dim().width - 16;
456            let height = editor.dim().height - 16;
457
458            if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
459                let grid = (24_f32 * self.zoom) as i32;
460
461                rgba_view.set_grid(Some(grid));
462
463                let mut filtered_tiles = vec![];
464
465                for (_, t) in tiles {
466                    if t.tags.to_lowercase().contains(&self.filter)
467                        && (self.filter_role == 0
468                            || t.role == TileRole::from_index(self.filter_role - 1))
469                    {
470                        filtered_tiles.push(t);
471                    }
472                }
473
474                if grid == 0 || width <= 0 {
475                    return;
476                }
477                let tiles_per_row = width / grid;
478                let lines = filtered_tiles.len() as i32 / tiles_per_row + 1;
479
480                let mut buffer =
481                    TheRGBABuffer::new(TheDim::sized(width, (lines * grid).max(height)));
482
483                for (i, tile) in filtered_tiles.iter().enumerate() {
484                    let x = i as i32 % tiles_per_row;
485                    let y = i as i32 / tiles_per_row;
486
487                    self.tile_ids.insert((x, y), tile.id);
488                    if !tile.textures.is_empty() {
489                        let tex = &tile.textures[0];
490                        let stride = buffer.stride();
491                        ctx.draw.blend_scale_chunk(
492                            buffer.pixels_mut(),
493                            &(
494                                x as usize * grid as usize,
495                                y as usize * grid as usize,
496                                grid as usize,
497                                grid as usize,
498                            ),
499                            stride,
500                            &tex.data,
501                            &(tex.width, tex.height),
502                        );
503                    }
504                }
505
506                rgba_view.set_buffer(buffer);
507            }
508            editor.relayout(ctx);
509        }
510    }
511}