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 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_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 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 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 let scale = (dst_w / src_w).min(dst_h / src_h);
410
411 let draw_w = src_w * scale;
413 let draw_h = src_h * scale;
414
415 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 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}