1use crate::editor::{RUSTERIX, SCENEMANAGER};
2use crate::prelude::*;
3use rusterix::TileRole;
4
5#[allow(dead_code)]
6#[derive(PartialEq)]
7enum AddMode {
8 Single,
9 Anim,
10 Multi,
11}
12
13use AddMode::*;
14
15pub struct TilemapDock {
16 curr_tilemap_id: Uuid,
17 add_mode: AddMode,
18
19 preview_tile: Option<rusterix::Tile>,
20}
21
22impl Dock for TilemapDock {
23 fn new() -> Self
24 where
25 Self: Sized,
26 {
27 Self {
28 curr_tilemap_id: Uuid::new_v4(),
29 add_mode: Single,
30
31 preview_tile: None,
32 }
33 }
34
35 fn setup(&mut self, _ctx: &mut TheContext) -> TheCanvas {
36 let mut canvas = TheCanvas::new();
37
38 let rgba_layout = TheRGBALayout::new(TheId::named("Tilemap Editor"));
39 canvas.set_layout(rgba_layout);
40
41 let mut toolbar_canvas = TheCanvas::new();
42 let traybar_widget = TheTraybar::new(TheId::empty());
43 toolbar_canvas.set_widget(traybar_widget);
44
45 let mut clear_button = TheTraybarButton::new(TheId::named("Tilemap Editor Clear"));
46 clear_button.set_text(fl!("clear"));
47 clear_button.set_status_text(&fl!("status_tilemap_clear_button"));
48
49 let mut toolbar_hlayout = TheHLayout::new(TheId::empty());
52 toolbar_hlayout.set_background_color(None);
53 toolbar_hlayout.set_margin(Vec4::new(10, 4, 5, 4));
54
55 let mut drop_down = TheDropdownMenu::new(TheId::named("Tilemap Editor Role"));
78
79 for dir in TileRole::iterator() {
80 drop_down.add_option(dir.to_string().to_string());
81 }
82 toolbar_hlayout.add_widget(Box::new(drop_down));
83
84 let mut hdivider = TheHDivider::new(TheId::empty());
85 hdivider.limiter_mut().set_max_width(15);
86 toolbar_hlayout.add_widget(Box::new(hdivider));
87
88 let mut add_switch = TheGroupButton::new(TheId::named("Tilemap Editor Switch"));
89 add_switch.add_text_status("Single".to_string(), "Show tile picker.".to_string());
90 add_switch.add_text_status(
91 "Anim".to_string(),
92 "Apply procedural materials.".to_string(),
93 );
94 add_switch.add_text_status("Multi".to_string(), "Apply a color.".to_string());
95
96 add_switch.set_item_width(70);
97 add_switch.set_index(0);
98 toolbar_hlayout.add_widget(Box::new(add_switch));
99
100 let mut hdivider = TheHDivider::new(TheId::empty());
101 hdivider.limiter_mut().set_max_width(15);
102 toolbar_hlayout.add_widget(Box::new(hdivider));
103
104 let mut add_button = TheTraybarButton::new(TheId::named("Tilemap Editor Add"));
105 add_button.set_text(fl!("tilemap_add_button"));
106 add_button.set_status_text(&fl!("status_tilemap_add_button"));
107
108 toolbar_hlayout.add_widget(Box::new(add_button));
109 toolbar_hlayout.add_widget(Box::new(clear_button));
110
111 let mut zoom = TheSlider::new(TheId::named("Tilemap Editor Zoom"));
116 zoom.set_value(TheValue::Float(2.0));
117 zoom.set_range(TheValue::RangeF32(0.5..=5.0));
118 zoom.set_continuous(true);
119 zoom.limiter_mut().set_max_width(120);
120 toolbar_hlayout.add_widget(Box::new(zoom));
121 toolbar_hlayout.set_reverse_index(Some(1));
122
123 let mut details_canvas = TheCanvas::new();
125
126 let mut vlayout = TheVLayout::new(TheId::named(" Tile Details Layout"));
127 vlayout.set_margin(Vec4::new(5, 20, 5, 10));
128 vlayout.set_alignment(TheHorizontalAlign::Center);
129 vlayout.limiter_mut().set_max_width(120);
130
131 let mut icon_preview = TheIconView::new(TheId::named("Tilemap Selection Preview"));
137 icon_preview.set_alpha_mode(false);
138 icon_preview.limiter_mut().set_max_size(Vec2::new(100, 100));
139 icon_preview.set_border_color(Some([100, 100, 100, 255]));
140
141 details_canvas.set_layout(vlayout);
145
146 toolbar_canvas.set_layout(toolbar_hlayout);
147 canvas.set_top(toolbar_canvas);
148 canvas
151 }
152
153 fn activate(
154 &mut self,
155 ui: &mut TheUI,
156 ctx: &mut TheContext,
157 project: &Project,
158 server_ctx: &mut ServerContext,
159 ) {
160 if let Some(id) = server_ctx.pc.id() {
161 if server_ctx.pc.is_tilemap() {
162 if let Some(tilemap) = project.get_tilemap(id) {
163 self.set_tilemap(tilemap, ui, ctx);
164 }
165 }
166 }
167 }
168
169 fn supports_actions(&self) -> bool {
170 false
171 }
172
173 fn default_state(&self) -> DockDefaultState {
174 DockDefaultState::Maximized
175 }
176
177 fn handle_event(
178 &mut self,
179 event: &TheEvent,
180 ui: &mut TheUI,
181 ctx: &mut TheContext,
182 project: &mut Project,
183 _server_ctx: &mut ServerContext,
184 ) -> bool {
185 let mut redraw = false;
186
187 match event {
188 TheEvent::Custom(id, value) => {
189 if id.name == "Tilemap Grid Size Changed" {
190 if let Some(rgba_layout) = ui.get_rgba_layout("Tilemap Editor") {
191 if let Some(rgba_view) = rgba_layout.rgba_view_mut().as_rgba_view() {
192 if let Some(grid_size) = value.to_i32() {
193 rgba_view.set_grid(Some(grid_size));
194 }
195 }
196 }
197 }
198 }
199 TheEvent::TileZoomBy(id, delta) => {
200 if id.name == "Tilemap Editor View" {
201 if let Some(tilemap) = project.get_tilemap_mut(self.curr_tilemap_id) {
202 tilemap.zoom += *delta * 0.05;
203 tilemap.zoom = tilemap.zoom.clamp(0.5, 5.0);
204 self.set_tilemap(tilemap, ui, ctx);
205 ctx.ui.relayout = true;
206 }
207 }
208 }
209 TheEvent::IndexChanged(id, index) => {
210 if id.name == "Tilemap Editor Switch" {
211 if *index == 0 {
212 self.add_mode = Single;
213 } else if *index == 1 {
214 self.add_mode = Anim;
215 } else {
216 self.add_mode = Multi;
217 }
218 self.compute_preview(project, ui);
219 redraw = true;
220 }
221 }
222 TheEvent::ContextMenuSelected(_widget_id, item_id) => {
223 if item_id.name == "Rename Tileset" {
224 if let Some(tilemap) = project.get_tilemap(self.curr_tilemap_id) {
225 open_text_dialog(
226 "Rename Tileset",
227 "Tilset Name",
228 tilemap.name.as_str(),
229 self.curr_tilemap_id,
230 ui,
231 ctx,
232 );
233 }
234 } else if item_id.name == "Add Tileset Colors" {
235 if let Some(tilemap) = project.get_tilemap(self.curr_tilemap_id).cloned() {
237 let width = tilemap.buffer.dim().width;
238 let height = tilemap.buffer.dim().height;
239 for y in 0..height {
240 for x in 0..width {
241 if let Some(c) = tilemap.buffer.get_pixel(x, y) {
242 let color = TheColor::from(c);
243 if color.a == 1.0 {
244 project.palette.add_unique_color(color);
245 }
246 }
247 }
248 }
249 }
250 if let Some(palette_picker) = ui.get_palette_picker("Palette Picker") {
251 let index = palette_picker.index();
252
253 palette_picker.set_palette(project.palette.clone());
254 if let Some(widget) = ui.get_widget("Palette Color Picker") {
255 if let Some(color) = &project.palette[index] {
256 widget.set_value(TheValue::ColorObject(color.clone()));
257 }
258 }
259 if let Some(widget) = ui.get_widget("Palette Hex Edit") {
260 if let Some(color) = &project.palette[index] {
261 widget.set_value(TheValue::Text(color.to_hex()));
262 }
263 }
264 }
265 redraw = true;
266 }
267 }
268 TheEvent::TileSelectionChanged(id) => {
269 if id.name == "Tilemap Editor View" {
270 self.compute_preview(project, ui);
271 }
272 }
273 TheEvent::StateChanged(id, state) => {
274 if id.name == "Tilemap Editor Clear" && *state == TheWidgetState::Clicked {
275 self.clear(ui);
276 } else if id.name == "Tilemap Editor Add" {
277 let clear_selection = true;
278
279 if let Some(editor) = ui
280 .canvas
281 .get_layout(Some(&"Tilemap Editor".to_string()), None)
282 {
283 if let Some(editor) = editor.as_rgba_layout() {
284 let mut tiles = vec![];
285
286 if self.add_mode == Single {
287 let sequence = editor
288 .rgba_view_mut()
289 .as_rgba_view()
290 .unwrap()
291 .selection_as_sequence();
292 for region in sequence.regions {
293 let mut tile = Tile::default();
294 let mut s = TheRGBARegionSequence::default();
295 s.regions.push(region);
296 tile.sequence = s;
297 tiles.push(tile);
298 }
299 } else if self.add_mode == Anim {
300 let mut tile = Tile::default();
301 let sequence = editor
302 .rgba_view_mut()
303 .as_rgba_view()
304 .unwrap()
305 .selection_as_sequence();
306 tile.sequence = sequence;
307 tiles.push(tile);
308 } else if self.add_mode == Multi {
309 let mut tile = Tile::default();
310 let dim = editor
311 .rgba_view_mut()
312 .as_rgba_view()
313 .unwrap()
314 .selection_as_dim();
315
316 let mut grid_size = 16;
317
318 if let Some(t) = project.get_tilemap(self.curr_tilemap_id) {
319 grid_size = t.grid_size;
320 }
321
322 let region = TheRGBARegion::new(
323 dim.x as usize * grid_size as usize,
324 dim.y as usize * grid_size as usize,
325 dim.width as usize * grid_size as usize,
326 dim.height as usize * grid_size as usize,
327 );
328
329 tile.sequence = TheRGBARegionSequence::new();
330 tile.sequence.regions.push(region);
331 tiles.push(tile);
332 }
333
334 for mut tile in tiles {
335 if let Some(role_widget) =
349 ui.get_drop_down_menu("Tilemap Editor Role")
350 {
351 let index = role_widget.selected_index();
352 tile.role = TileRole::from_index(index as u8);
353 }
354
355 if !tile.sequence.regions.is_empty() {
357 let id = tile.id;
402 if let Some(tilemap) =
404 project.get_tilemap_mut(self.curr_tilemap_id)
405 {
406 tilemap.tiles.push(tile);
407 }
409
410 if let Some(t) = project.extract_tile(&id) {
411 let mut texture_array: Vec<rusterix::Texture> = vec![];
413 for b in &t.buffer {
414 let mut texture = rusterix::Texture::new(
415 b.pixels().to_vec(),
416 b.dim().width as usize,
417 b.dim().height as usize,
418 );
419 texture.generate_normals(true);
420 texture_array.push(texture);
421 }
422 let mut tile = rusterix::Tile {
423 id: t.id,
424 role: rusterix::TileRole::from_index(t.role),
425 textures: texture_array.clone(),
426 module: None,
427 blocking: t.blocking,
428 scale: t.scale,
429 tags: t.name.clone(),
430 };
431 tile.set_default_materials();
432 project.tiles.insert(id, tile);
433 }
434
435 let mut rusterix = RUSTERIX.write().unwrap();
436 rusterix.set_tiles(project.tiles.clone(), true);
437 SCENEMANAGER.write().unwrap().set_tile_list(
438 rusterix.assets.tile_list.clone(),
439 rusterix.assets.tile_indices.clone(),
440 );
441
442 ctx.ui.send(TheEvent::Custom(
443 TheId::named("Update Tilepicker"),
444 TheValue::Empty,
445 ));
446 }
448 }
449 }
450 }
451
452 if clear_selection {
454 if let Some(editor) = ui
455 .canvas
456 .get_layout(Some(&"Tilemap Editor".to_string()), None)
457 {
458 if let Some(editor) = editor.as_rgba_layout() {
459 editor
460 .rgba_view_mut()
461 .as_rgba_view()
462 .unwrap()
463 .set_selection(FxHashSet::default());
464 }
465 ctx.ui.send(TheEvent::StateChanged(
466 TheId::named("Tilemap Editor Clear"),
467 TheWidgetState::Clicked,
468 ))
469 }
470 }
471 }
472 }
473 TheEvent::KeyCodeDown(TheValue::KeyCode(key)) => {
474 if *key == TheKeyCode::Escape {
475 self.clear(ui);
476 }
477 }
478 TheEvent::ValueChanged(_id, _value) => {}
479 _ => {}
480 }
481 redraw
482 }
483
484 fn draw_minimap(
486 &self,
487 buffer: &mut TheRGBABuffer,
488 _project: &Project,
489 ctx: &mut TheContext,
490 server_ctx: &ServerContext,
491 ) -> bool {
492 if let Some(tile) = &self.preview_tile
493 && !tile.textures.is_empty()
494 {
495 buffer.fill(BLACK);
496 let index = server_ctx.animation_counter % tile.textures.len();
497
498 let stride: usize = buffer.stride();
499
500 let src_pixels = &tile.textures[index].data;
501 let src_w = tile.textures[index].width as f32;
502 let src_h = tile.textures[index].height as f32;
503
504 let dim = buffer.dim();
505 let dst_w = dim.width as f32;
506 let dst_h = dim.height as f32;
507
508 let scale = (dst_w / src_w).min(dst_h / src_h);
510
511 let draw_w = src_w * scale;
513 let draw_h = src_h * scale;
514
515 let offset_x = ((dst_w - draw_w) * 0.5).round() as usize;
517 let offset_y = ((dst_h - draw_h) * 0.5).round() as usize;
518
519 let dst_rect = (
520 offset_x,
521 offset_y,
522 draw_w.round() as usize,
523 draw_h.round() as usize,
524 );
525
526 ctx.draw.blend_scale_chunk(
527 buffer.pixels_mut(),
528 &dst_rect,
529 stride,
530 src_pixels,
531 &(src_w as usize, src_h as usize),
532 );
533
534 return true;
535 }
536
537 false
538 }
539
540 fn supports_minimap_animation(&self) -> bool {
541 true
542 }
543}
544
545impl TilemapDock {
546 pub fn set_tilemap(
548 &mut self,
549 tilemap: &tilemap::Tilemap,
550 ui: &mut TheUI,
551 ctx: &mut TheContext,
552 ) {
553 self.curr_tilemap_id = tilemap.id;
554
555 ui.set_widget_value("Tilemap Editor Zoom", ctx, TheValue::Float(tilemap.zoom));
556
557 if let Some(rgba_layout) = ui.get_rgba_layout("Tilemap Editor") {
558 rgba_layout.set_buffer(tilemap.buffer.clone());
559 rgba_layout.set_scroll_offset(tilemap.scroll_offset);
560 if let Some(rgba_view) = rgba_layout.rgba_view_mut().as_rgba_view() {
561 rgba_view.set_supports_external_zoom(true);
562 rgba_view.set_grid(Some(tilemap.grid_size));
563 rgba_view.set_mode(TheRGBAViewMode::TileSelection);
564 rgba_view.set_background([116, 116, 116, 255]);
565 let mut c = WHITE;
566 c[3] = 128;
567 rgba_view.set_hover_color(Some(c));
568 rgba_view.set_rectangular_selection(true);
569 rgba_view.set_dont_show_grid(true);
570 rgba_view.set_zoom(tilemap.zoom);
571
572 let mut used = FxHashSet::default();
573
574 for tile in &tilemap.tiles {
576 for region in &tile.sequence.regions {
577 used.insert((
578 region.x as i32 / tilemap.grid_size,
579 region.y as i32 / tilemap.grid_size,
580 ));
581 }
582 }
583 rgba_view.set_used(used);
584 }
585 }
586 }
587
588 pub fn clear(&mut self, ui: &mut TheUI) {
590 if let Some(editor) = ui
591 .canvas
592 .get_layout(Some(&"Tilemap Editor".to_string()), None)
593 {
594 if let Some(editor) = editor.as_rgba_layout() {
595 editor
596 .rgba_view_mut()
597 .as_rgba_view()
598 .unwrap()
599 .set_selection(FxHashSet::default());
600 }
601 }
602 self.set_tilemap_preview(TheRGBATile::default(), ui);
603 }
604
605 pub fn set_tilemap_preview(&mut self, rgba_tile: TheRGBATile, _ui: &mut TheUI) {
607 let mut texture_array: Vec<rusterix::Texture> = vec![];
608 for b in &rgba_tile.buffer {
609 let mut texture = rusterix::Texture::new(
610 b.pixels().to_vec(),
611 b.dim().width as usize,
612 b.dim().height as usize,
613 );
614 texture.generate_normals(true);
615 texture_array.push(texture);
616 }
617 self.preview_tile = Some(rusterix::Tile {
618 id: rgba_tile.id,
619 role: rusterix::TileRole::from_index(rgba_tile.role),
620 textures: texture_array.clone(),
621 module: None,
622 blocking: rgba_tile.blocking,
623 scale: rgba_tile.scale,
624 tags: rgba_tile.name.clone(),
625 });
626
627 }
638
639 pub fn compute_preview(&mut self, project: &mut Project, ui: &mut TheUI) {
641 if let Some(rgba_layout) = ui.get_rgba_layout("Tilemap Editor") {
642 if let Some(rgba_view) = rgba_layout.rgba_view_mut().as_rgba_view() {
643 if self.add_mode == Single {
644 let mut tile = rgba_view.selection_as_tile();
645 if let Some(last) = tile.buffer.last() {
646 tile.buffer = vec![last.clone()];
647 }
648 self.set_tilemap_preview(tile, ui);
649 } else if self.add_mode == Anim {
650 let selection = rgba_view.selection_as_sequence();
651 let mut tile = TheRGBATile::default();
652 if let Some(tilemap) = project.get_tilemap(self.curr_tilemap_id) {
653 tile.buffer = tilemap.buffer.extract_sequence(&selection);
654 }
655 self.set_tilemap_preview(tile, ui);
656 } else {
657 let mut tile = TheRGBATile::default();
658 let dim = rgba_view.selection_as_dim();
659
660 if let Some(tilemap) = project.get_tilemap(self.curr_tilemap_id) {
661 let region = TheRGBARegion::new(
662 dim.x as usize * tilemap.grid_size as usize,
663 dim.y as usize * tilemap.grid_size as usize,
664 dim.width as usize * tilemap.grid_size as usize,
665 dim.height as usize * tilemap.grid_size as usize,
666 );
667 tile.buffer.push(tilemap.buffer.extract_region(®ion));
668 }
669 self.set_tilemap_preview(tile, ui);
670 }
671 }
672 }
673 }
674}