1use crate::docks::tiles_editor_undo::*;
2use crate::editor::TOOLLIST;
3use crate::prelude::*;
4
5pub struct TilesEditorDock {
6 zoom: f32,
7 show_grid: bool,
8 tile_node: Uuid,
9 palette_node: Uuid,
10 grid_node: Uuid,
11 body_markers_node: Uuid,
12
13 tile_undos: FxHashMap<Uuid, TileEditorUndo>,
15 current_tile_id: Option<Uuid>,
16 current_undo_key: Option<Uuid>,
18 max_undo: usize,
19
20 anim_preview: bool,
22 paste_preview_texture: Option<rusterix::Texture>,
23 paste_preview_pos: Option<Vec2<i32>>,
24}
25
26impl Dock for TilesEditorDock {
27 fn new() -> Self
28 where
29 Self: Sized,
30 {
31 Self {
32 zoom: 5.0,
33 show_grid: true,
34 tile_node: Uuid::new_v4(),
35 palette_node: Uuid::new_v4(),
36 grid_node: Uuid::new_v4(),
37 body_markers_node: Uuid::new_v4(),
38 tile_undos: FxHashMap::default(),
39 current_tile_id: None,
40 current_undo_key: None,
41 max_undo: 30,
42 anim_preview: false,
43 paste_preview_texture: None,
44 paste_preview_pos: None,
45 }
46 }
47
48 fn setup(&mut self, _ctx: &mut TheContext) -> TheCanvas {
49 let mut canvas = TheCanvas::new();
50
51 let mut rgba_layout = TheRGBALayout::new(TheId::named("Tile Editor Dock RGBA Layout"));
52 if let Some(rgba_view) = rgba_layout.rgba_view_mut().as_rgba_view() {
53 rgba_view.set_supports_external_zoom(true);
54 rgba_view.set_background([116, 116, 116, 255]);
55 rgba_view.set_dont_show_grid(!self.show_grid);
59 rgba_view.set_show_transparency(true);
60 rgba_view.set_mode(TheRGBAViewMode::TileEditor);
61 let mut c = WHITE;
62 c[3] = 128;
63 rgba_view.set_hover_color(Some(c));
64 }
65
66 canvas.set_layout(rgba_layout);
67
68 let mut stack_canvas = TheCanvas::new();
69 let mut stack_layout = TheStackLayout::new(TheId::named("Pixel Editor Stack Layout"));
70 stack_layout.limiter_mut().set_max_width(305);
71
72 let mut palette_canvas = TheCanvas::default();
75 let mut palette_tree_layout = TheTreeLayout::new(TheId::named("Tile Editor Tree"));
76 palette_tree_layout.limiter_mut().set_max_width(305);
77 let root = palette_tree_layout.get_root();
78
79 let mut tile_node: TheTreeNode =
81 TheTreeNode::new(TheId::named_with_id("Tile", self.tile_node));
82 tile_node.set_open(true);
83
84 let mut item = TheTreeItem::new(TheId::named("Tile Size"));
85 item.set_text(fl!("size"));
86
87 let mut edit = TheTextLineEdit::new(TheId::named("Tile Size Edit"));
88 edit.set_value(TheValue::Int(0));
89 item.add_widget_column(150, Box::new(edit));
90 tile_node.add_widget(Box::new(item));
91
92 let mut item = TheTreeItem::new(TheId::named("Tile Frames"));
93 item.set_text(fl!("frames"));
94
95 let mut edit = TheTextLineEdit::new(TheId::named("Tile Frame Edit"));
96 edit.set_value(TheValue::Int(0));
97 item.add_widget_column(150, Box::new(edit));
98 tile_node.add_widget(Box::new(item));
99
100 let mut item = TheTreeIcons::new(TheId::named("Tile Frame Icons"));
101 item.set_icon_size(40);
102 item.set_icon_count(1);
103 item.set_selected_index(Some(0));
104 tile_node.add_widget(Box::new(item));
105
106 root.add_child(tile_node);
107
108 let mut palette_node: TheTreeNode =
111 TheTreeNode::new(TheId::named_with_id("Color", self.palette_node));
112 palette_node.set_open(true);
113
114 let mut item = TheTreeItem::new(TheId::named("Palette Opacity"));
115 item.set_text(fl!("opacity"));
116
117 let mut edit = TheTextLineEdit::new(TheId::named("Palette Opacity Edit"));
118 edit.set_value(TheValue::Float(1.0));
119 edit.set_range(TheValue::RangeF32(0.0..=1.0));
120 item.add_widget_column(150, Box::new(edit));
121 palette_node.add_widget(Box::new(item));
122
123 root.add_child(palette_node);
130
131 let mut grid_node: TheTreeNode =
133 TheTreeNode::new(TheId::named_with_id("Grid", self.grid_node));
134 grid_node.set_open(true);
135
136 let mut item = TheTreeItem::new(TheId::named("Grid Enabled"));
137 let mut cb = TheCheckButton::new(TheId::named("Grid Enabled CB"));
138 cb.set_state(TheWidgetState::Selected);
139 item.add_widget_column(150, Box::new(cb));
140 item.set_text(fl!("enabled"));
141
142 grid_node.add_widget(Box::new(item));
143
144 root.add_child(grid_node);
145
146 palette_canvas.set_layout(palette_tree_layout);
149
150 stack_layout.add_canvas(palette_canvas);
151
152 let mut avatar_canvas = TheCanvas::default();
155 let mut avatar_tree_layout = TheTreeLayout::new(TheId::named("Avatar Editor Tree"));
156 avatar_tree_layout.limiter_mut().set_max_width(305);
157 let root = avatar_tree_layout.get_root();
158
159 let mut body_markers_node: TheTreeNode = TheTreeNode::new(TheId::named_with_id(
160 &fl!("body_markers"),
161 self.body_markers_node,
162 ));
163 body_markers_node.set_open(true);
164
165 let mut item = TheTreeItem::new(TheId::named("Body: Skin Light"));
176 item.set_text(fl!("skin_light"));
177 item.set_background_color(TheColor::from_u8_array_3([255, 0, 255]));
178 body_markers_node.add_widget(Box::new(item));
179
180 let mut item = TheTreeItem::new(TheId::named("Body: Skin Dark"));
181 item.set_text(fl!("skin_dark"));
182 item.set_background_color(TheColor::from_u8_array_3([200, 0, 200]));
183 body_markers_node.add_widget(Box::new(item));
184
185 let mut item = TheTreeItem::new(TheId::named("Body: Torso"));
186 item.set_text(fl!("torso"));
187 item.set_background_color(TheColor::from_u8_array_3([0, 0, 255]));
188 body_markers_node.add_widget(Box::new(item));
189
190 let mut item = TheTreeItem::new(TheId::named("Body: Arms"));
191 item.set_text(fl!("arms"));
192 item.set_background_color(TheColor::from_u8_array_3([0, 120, 255]));
193 body_markers_node.add_widget(Box::new(item));
194
195 let mut item = TheTreeItem::new(TheId::named("Body: Legs"));
196 item.set_text(fl!("legs"));
197 item.set_background_color(TheColor::from_u8_array_3([0, 255, 0]));
198 body_markers_node.add_widget(Box::new(item));
199
200 let mut item = TheTreeItem::new(TheId::named("Body: Hair"));
201 item.set_text(fl!("hair"));
202 item.set_background_color(TheColor::from_u8_array_3([255, 255, 0]));
203 body_markers_node.add_widget(Box::new(item));
204
205 let mut item = TheTreeItem::new(TheId::named("Body: Eyes"));
206 item.set_text(fl!("eyes"));
207 item.set_background_color(TheColor::from_u8_array_3([0, 255, 255]));
208 body_markers_node.add_widget(Box::new(item));
209
210 let mut item = TheTreeItem::new(TheId::named("Body: Hands"));
211 item.set_text(fl!("hands"));
212 item.set_background_color(TheColor::from_u8_array_3([255, 128, 0]));
213 body_markers_node.add_widget(Box::new(item));
214
215 let mut item = TheTreeItem::new(TheId::named("Body: Feet"));
216 item.set_text(fl!("feet"));
217 item.set_background_color(TheColor::from_u8_array_3([255, 80, 0]));
218 body_markers_node.add_widget(Box::new(item));
219
220 root.add_child(body_markers_node);
221
222 let mut anchors_node: TheTreeNode = TheTreeNode::new(TheId::named(&fl!("anchors")));
223 anchors_node.set_open(true);
224
225 let mut item = TheTreeItem::new(TheId::named("Anchor: Main"));
226 item.set_text(fl!("avatar_anchor_main"));
227 anchors_node.add_widget(Box::new(item));
228
229 let mut item = TheTreeItem::new(TheId::named("Anchor: Off"));
230 item.set_text(fl!("avatar_anchor_off"));
231 anchors_node.add_widget(Box::new(item));
232
233 root.add_child(anchors_node);
239
240 avatar_canvas.set_layout(avatar_tree_layout);
241
242 stack_layout.add_canvas(avatar_canvas);
243
244 stack_canvas.set_layout(stack_layout);
245 canvas.set_left(stack_canvas);
246
247 canvas
248 }
249
250 fn activate(
251 &mut self,
252 ui: &mut TheUI,
253 ctx: &mut TheContext,
254 project: &Project,
255 server_ctx: &mut ServerContext,
256 ) {
257 self.editing_context_changed(ui, ctx, project, server_ctx);
258 }
259
260 fn minimized(&mut self, _ui: &mut TheUI, ctx: &mut TheContext) {
261 ctx.ui.send(TheEvent::Custom(
262 TheId::named("Update Tiles"),
263 TheValue::Empty,
264 ));
265 }
266
267 fn handle_event(
268 &mut self,
269 event: &TheEvent,
270 ui: &mut TheUI,
271 ctx: &mut TheContext,
272 project: &mut Project,
273 server_ctx: &mut ServerContext,
274 ) -> bool {
275 if server_ctx.help_mode {
276 let open_tile_help = match event {
277 TheEvent::TileEditorClicked(id, _) => {
278 id.name == "Tile Editor Dock RGBA Layout View"
279 }
280 TheEvent::StateChanged(id, state) if *state == TheWidgetState::Clicked => {
281 id.name.starts_with("Tile ")
282 || id.name == "Grid Enabled CB"
283 || id.name == "Tile Editor Tree"
284 }
285 TheEvent::MouseDown(coord) => ui
286 .get_widget_at_coord(*coord)
287 .map(|w| {
288 let name = &w.id().name;
289 name.starts_with("Tile ")
290 || name == "Tile Editor Dock RGBA Layout View"
291 || name == "Tile Editor Tree"
292 || name == "Grid Enabled CB"
293 })
294 .unwrap_or(false),
295 _ => false,
296 };
297
298 if open_tile_help {
299 ctx.ui.send(TheEvent::Custom(
300 TheId::named("Show Help"),
301 TheValue::Text("docs/creator/docks/tile_picker_editor".into()),
302 ));
303 return true;
304 }
305 }
306
307 let mut redraw = false;
308
309 match event {
310 TheEvent::Custom(id, value) => {
311 if let TheValue::Id(tile_id) = value
312 && id.name == "Tile Picked"
313 {
314 if let Some(tile) = project.tiles.get(tile_id) {
315 self.set_tile(tile, ui, ctx, server_ctx, false);
316 }
317 self.editing_context_changed(ui, ctx, project, server_ctx);
318 } else if let TheValue::Id(tile_id) = value
319 && id.name == "Tile Updated"
320 {
321 if let Some(tile) = project.tiles.get(tile_id) {
322 self.set_tile(tile, ui, ctx, server_ctx, true);
323
324 if let Some(tree_layout) = ui.get_tree_layout("Tile Editor Tree") {
326 if let Some(tile_node) = tree_layout.get_node_by_id_mut(&self.tile_node)
327 {
328 if let Some(widget) = tile_node.widgets[2].as_tree_icons() {
330 if server_ctx.curr_tile_frame_index < tile.textures.len() {
331 widget.set_icon(
332 server_ctx.curr_tile_frame_index,
333 tile.textures[server_ctx.curr_tile_frame_index]
334 .to_rgba(),
335 );
336 }
337 }
338 }
339 }
340 }
341 } else if id.name == "Editing Texture Updated" {
342 self.refresh_from_editing_context(project, ui, ctx, server_ctx);
343 } else if id.name == "Tile Editor Undo Available" {
344 if let Some(atom) = TOOLLIST
345 .write()
346 .unwrap()
347 .get_current_editor_tool()
348 .get_undo_atom(project)
349 {
350 if let Some(atom) = atom.downcast_ref::<TileEditorUndoAtom>() {
351 self.add_undo(atom.clone(), ctx);
352 }
353 }
354 }
355 }
356 TheEvent::ValueChanged(id, value) => {
357 if id.name == "Tile Size Edit" {
359 if let Some(size) = value.to_i32() {
360 if let Some(tile_id) = self.current_tile_id {
361 if let Some(tile) = project.tiles.get_mut(&tile_id) {
362 if !tile.is_empty() {
363 if size != tile.textures[0].width as i32 {
364 let new_tile = tile.resized(size as usize, size as usize);
365 let atom = TileEditorUndoAtom::TileEdit(
366 tile.id,
367 tile.clone(),
368 new_tile.clone(),
369 );
370 *tile = new_tile;
371 self.add_undo(atom, ctx);
372 self.set_tile(tile, ui, ctx, server_ctx, false);
373 }
374 }
375 }
376 }
377 }
378 } else
379 if id.name == "Tile Frame Edit" {
381 if let Some(frames) = value.to_i32() {
382 if let Some(tile_id) = self.current_tile_id {
383 if let Some(tile) = project.tiles.get_mut(&tile_id) {
384 if frames != tile.textures.len() as i32 {
385 let mut new_tile = tile.clone();
386 new_tile.set_frames(frames as usize);
387 let atom = TileEditorUndoAtom::TileEdit(
388 tile.id,
389 tile.clone(),
390 new_tile.clone(),
391 );
392 *tile = new_tile;
393 self.add_undo(atom, ctx);
394 self.set_tile(tile, ui, ctx, server_ctx, false);
395 }
396 }
397 }
398 }
399 } else
400 if id.name == "Palette Opacity Edit" {
402 if let Some(opacity) = value.to_f32() {
403 server_ctx.palette_opacity = opacity;
404 }
405 }
406 }
407 TheEvent::IndexChanged(id, index) => {
408 if id.name == "Tile Frame Icons" {
409 self.set_frame_index(*index as usize, project, ui, ctx, server_ctx);
411 }
412 }
416 TheEvent::StateChanged(id, state) => {
417 if id.name == "Grid Enabled CB" {
418 self.show_grid = *state == TheWidgetState::Selected;
419 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout")
420 && let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view()
421 {
422 rgba_view.set_dont_show_grid(!self.show_grid);
423 editor.relayout(ctx);
424 }
425 redraw = true;
426 } else if *state == TheWidgetState::Selected && id.name.starts_with("Body: ") {
427 server_ctx.avatar_anchor_slot = AvatarAnchorEditSlot::None;
428 let color = match id.name.as_str() {
429 "Body: Skin Light" => Some([255, 0, 255, 255]),
430 "Body: Skin Dark" => Some([200, 0, 200, 255]),
431 "Body: Torso" => Some([0, 0, 255, 255]),
432 "Body: Legs" => Some([0, 255, 0, 255]),
433 "Body: Hair" => Some([255, 255, 0, 255]),
434 "Body: Eyes" => Some([0, 255, 255, 255]),
435 "Body: Hands" => Some([255, 128, 0, 255]),
436 "Body: Feet" => Some([255, 80, 0, 255]),
437 _ => None,
438 };
439 if let Some(c) = color {
440 server_ctx.body_marker_color = Some(c);
441 redraw = true;
442 }
443 } else if *state == TheWidgetState::Selected && id.name == "Anchor: Main" {
444 server_ctx.avatar_anchor_slot = AvatarAnchorEditSlot::Main;
445 self.sync_anchor_overlay(project, ui, ctx, server_ctx);
446 redraw = true;
447 } else if *state == TheWidgetState::Selected && id.name == "Anchor: Off" {
448 server_ctx.avatar_anchor_slot = AvatarAnchorEditSlot::Off;
449 self.sync_anchor_overlay(project, ui, ctx, server_ctx);
450 redraw = true;
451 }
452 }
453 TheEvent::TileZoomBy(id, delta) => {
454 if id.name == "Tile Editor Dock RGBA Layout View" {
455 self.zoom += *delta * 0.5;
456 self.zoom = self.zoom.clamp(1.0, 60.0);
457 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
458 editor.set_zoom(self.zoom);
459 editor.relayout(ctx);
460 }
461 }
462 }
463 TheEvent::TileEditorHoverChanged(id, pos) => {
464 if id.name == "Tile Editor Dock RGBA Layout View"
465 && self.paste_preview_texture.is_some()
466 {
467 self.paste_preview_pos = Some(*pos);
468 self.sync_paste_preview(ui, ctx);
469 redraw = true;
470 }
471 }
472 TheEvent::TileEditorClicked(id, coord) => {
473 if id.name == "Tile Editor Dock RGBA Layout View"
474 && self.paste_preview_texture.is_some()
475 {
476 self.paste_preview_pos = Some(*coord);
477 if self.apply_paste_at_preview(project, ui, ctx, server_ctx) {
478 self.clear_paste_preview(ui, ctx);
479 ctx.ui.send(TheEvent::SetStatusText(
480 TheId::empty(),
481 fl!("status_tile_editor_paste_applied"),
482 ));
483 } else {
484 ctx.ui.send(TheEvent::SetStatusText(
485 TheId::empty(),
486 fl!("status_tile_editor_paste_no_valid_target"),
487 ));
488 }
489 redraw = true;
490 } else if id.name == "Tile Editor Dock RGBA Layout View"
491 && matches!(server_ctx.editing_ctx, PixelEditingContext::AvatarFrame(..))
492 && server_ctx.avatar_anchor_slot != AvatarAnchorEditSlot::None
493 && self.apply_avatar_anchor_at(*coord, project, ctx, server_ctx)
494 {
495 self.sync_anchor_overlay(project, ui, ctx, server_ctx);
496 redraw = true;
497 }
498 }
499 TheEvent::Copy => {
500 if server_ctx.editing_ctx != PixelEditingContext::None {
501 if let Some(texture) = project.get_editing_texture(&server_ctx.editing_ctx) {
502 let selection = if let Some(editor) =
503 ui.get_rgba_layout("Tile Editor Dock RGBA Layout")
504 {
505 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
506 rgba_view.selection()
507 } else {
508 FxHashSet::default()
509 }
510 } else {
511 FxHashSet::default()
512 };
513
514 if let Ok(mut clipboard) = arboard::Clipboard::new() {
515 if selection.is_empty() {
516 let img = arboard::ImageData {
517 width: texture.width,
518 height: texture.height,
519 bytes: std::borrow::Cow::Borrowed(&texture.data),
520 };
521 let _ = clipboard.set_image(img);
522 ctx.ui.send(TheEvent::SetStatusText(
523 TheId::empty(),
524 fl!("status_tile_editor_copy_texture"),
525 ));
526 } else {
527 let min_x = selection.iter().map(|(x, _)| *x).min().unwrap_or(0);
528 let max_x = selection.iter().map(|(x, _)| *x).max().unwrap_or(0);
529 let min_y = selection.iter().map(|(_, y)| *y).min().unwrap_or(0);
530 let max_y = selection.iter().map(|(_, y)| *y).max().unwrap_or(0);
531
532 let out_w = (max_x - min_x + 1).max(1) as usize;
533 let out_h = (max_y - min_y + 1).max(1) as usize;
534 let mut out = vec![0_u8; out_w * out_h * 4];
535
536 for (x, y) in selection {
537 if x >= 0
538 && y >= 0
539 && (x as usize) < texture.width
540 && (y as usize) < texture.height
541 {
542 let src_i =
543 ((y as usize) * texture.width + (x as usize)) * 4;
544 let dx = (x - min_x) as usize;
545 let dy = (y - min_y) as usize;
546 let dst_i = (dy * out_w + dx) * 4;
547 out[dst_i..dst_i + 4]
548 .copy_from_slice(&texture.data[src_i..src_i + 4]);
549 }
550 }
551
552 let img = arboard::ImageData {
553 width: out_w,
554 height: out_h,
555 bytes: std::borrow::Cow::Owned(out),
556 };
557 let _ = clipboard.set_image(img);
558 ctx.ui.send(TheEvent::SetStatusText(
559 TheId::empty(),
560 fl!("status_tile_editor_copy_selection"),
561 ));
562 }
563 }
564 }
565 }
566 }
567 TheEvent::Cut => {
568 if server_ctx.editing_ctx != PixelEditingContext::None {
569 let selection =
570 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
571 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
572 rgba_view.selection()
573 } else {
574 FxHashSet::default()
575 }
576 } else {
577 FxHashSet::default()
578 };
579
580 if selection.is_empty() {
581 return redraw;
582 }
583
584 if let Some(texture) = project.get_editing_texture(&server_ctx.editing_ctx) {
586 if let Ok(mut clipboard) = arboard::Clipboard::new() {
587 let min_x = selection.iter().map(|(x, _)| *x).min().unwrap_or(0);
588 let max_x = selection.iter().map(|(x, _)| *x).max().unwrap_or(0);
589 let min_y = selection.iter().map(|(_, y)| *y).min().unwrap_or(0);
590 let max_y = selection.iter().map(|(_, y)| *y).max().unwrap_or(0);
591
592 let out_w = (max_x - min_x + 1).max(1) as usize;
593 let out_h = (max_y - min_y + 1).max(1) as usize;
594 let mut out = vec![0_u8; out_w * out_h * 4];
595
596 for (x, y) in &selection {
597 if *x >= 0
598 && *y >= 0
599 && (*x as usize) < texture.width
600 && (*y as usize) < texture.height
601 {
602 let src_i = ((*y as usize) * texture.width + (*x as usize)) * 4;
603 let dx = (*x - min_x) as usize;
604 let dy = (*y - min_y) as usize;
605 let dst_i = (dy * out_w + dx) * 4;
606 out[dst_i..dst_i + 4]
607 .copy_from_slice(&texture.data[src_i..src_i + 4]);
608 }
609 }
610
611 let img = arboard::ImageData {
612 width: out_w,
613 height: out_h,
614 bytes: std::borrow::Cow::Owned(out),
615 };
616 let _ = clipboard.set_image(img);
617 ctx.ui.send(TheEvent::SetStatusText(
618 TheId::empty(),
619 fl!("status_tile_editor_cut_selection"),
620 ));
621 }
622 }
623
624 if self.clear_current_selection(project, ui, ctx, server_ctx) {
625 redraw = true;
626 }
627 }
628 }
629 TheEvent::Paste(_, _) => {
630 if server_ctx.editing_ctx != PixelEditingContext::None {
631 if let Ok(mut clipboard) = arboard::Clipboard::new() {
632 if let Ok(img) = clipboard.get_image() {
633 let width = img.width;
635 let height = img.height;
636 let data: Vec<u8> = img.bytes.into_owned();
637
638 if width > 0 && height > 0 {
639 let pasted = rusterix::Texture::new(data, width, height);
640 self.paste_preview_texture = Some(pasted);
641 if self.paste_preview_pos.is_none() {
642 if let Some(texture) =
643 project.get_editing_texture(&server_ctx.editing_ctx)
644 {
645 self.paste_preview_pos = Some(Vec2::new(
646 texture.width as i32 / 2,
647 texture.height as i32 / 2,
648 ));
649 } else {
650 self.paste_preview_pos = Some(Vec2::zero());
651 }
652 }
653 self.sync_paste_preview(ui, ctx);
654 ctx.ui.send(TheEvent::SetStatusText(
655 TheId::empty(),
656 fl!("status_tile_editor_paste_preview_active"),
657 ));
658 redraw = true;
659 }
660 }
661 }
662 }
663 }
664 TheEvent::KeyCodeDown(TheValue::KeyCode(key)) => {
665 if *key == TheKeyCode::Escape && self.paste_preview_texture.is_some() {
666 self.clear_paste_preview(ui, ctx);
667 ctx.ui.send(TheEvent::SetStatusText(
668 TheId::empty(),
669 fl!("status_tile_editor_paste_preview_canceled"),
670 ));
671 redraw = true;
672 } else if *key == TheKeyCode::Return && self.paste_preview_texture.is_some() {
673 if self.apply_paste_at_preview(project, ui, ctx, server_ctx) {
674 self.clear_paste_preview(ui, ctx);
675 ctx.ui.send(TheEvent::SetStatusText(
676 TheId::empty(),
677 fl!("status_tile_editor_paste_applied"),
678 ));
679 redraw = true;
680 } else {
681 ctx.ui.send(TheEvent::SetStatusText(
682 TheId::empty(),
683 fl!("status_tile_editor_paste_no_valid_target"),
684 ));
685 }
686 } else if *key == TheKeyCode::Delete
687 && !ui.focus_widget_supports_text_input(ctx)
688 && self.paste_preview_texture.is_none()
689 {
690 if self.clear_current_selection(project, ui, ctx, server_ctx) {
691 redraw = true;
692 }
693 } else if *key == TheKeyCode::Space && !ui.focus_widget_supports_text_input(ctx) {
694 if server_ctx.editing_ctx != PixelEditingContext::None {
695 self.anim_preview = !self.anim_preview;
696 ctx.ui.send(TheEvent::Custom(
697 TheId::named("Update Minimap"),
698 TheValue::Empty,
699 ));
700 redraw = true;
701 }
702 }
703 }
704 TheEvent::KeyDown(TheValue::Char(c)) => {
705 if !ui.focus_widget_supports_text_input(ctx) {
706 let c = c.to_ascii_lowercase();
707 if c == 'h' {
708 if self.apply_flip(true, project, ui, ctx, server_ctx) {
709 redraw = true;
710 }
711 } else if c == 'v' && self.apply_flip(false, project, ui, ctx, server_ctx) {
712 redraw = true;
713 }
714 }
715 }
716 _ => {}
717 }
718
719 redraw
720 }
721
722 fn supports_undo(&self) -> bool {
723 true
724 }
725
726 fn has_changes(&self) -> bool {
727 self.tile_undos.values().any(|undo| undo.has_changes())
729 }
730
731 fn mark_saved(&mut self) {
732 for undo in self.tile_undos.values_mut() {
733 undo.index = -1;
734 }
735 }
736
737 fn undo(
738 &mut self,
739 ui: &mut TheUI,
740 ctx: &mut TheContext,
741 project: &mut Project,
742 _server_ctx: &mut ServerContext,
743 ) {
744 if let Some(key) = self.current_undo_key {
745 if let Some(undo) = self.tile_undos.get_mut(&key) {
746 undo.undo(project, ui, ctx);
747 self.set_undo_state_to_ui(ctx);
748 }
749 }
750 }
751
752 fn redo(
753 &mut self,
754 ui: &mut TheUI,
755 ctx: &mut TheContext,
756 project: &mut Project,
757 _server_ctx: &mut ServerContext,
758 ) {
759 if let Some(key) = self.current_undo_key {
760 if let Some(undo) = self.tile_undos.get_mut(&key) {
761 undo.redo(project, ui, ctx);
762 self.set_undo_state_to_ui(ctx);
763 }
764 }
765 }
766
767 fn set_undo_state_to_ui(&self, ctx: &mut TheContext) {
768 if let Some(key) = self.current_undo_key {
769 if let Some(undo) = self.tile_undos.get(&key) {
770 if undo.has_undo() {
771 ctx.ui.set_enabled("Undo");
772 } else {
773 ctx.ui.set_disabled("Undo");
774 }
775
776 if undo.has_redo() {
777 ctx.ui.set_enabled("Redo");
778 } else {
779 ctx.ui.set_disabled("Redo");
780 }
781 return;
782 }
783 }
784
785 ctx.ui.set_disabled("Undo");
787 ctx.ui.set_disabled("Redo");
788 }
789
790 fn editor_tools(&self) -> Option<Vec<Box<dyn EditorTool>>> {
791 Some(vec![
792 Box::new(TileDrawTool::new()),
793 Box::new(TileFillTool::new()),
794 Box::new(TilePickerTool::new()),
795 Box::new(TileEraserTool::new()),
796 Box::new(TileSelectTool::new()),
797 ])
798 }
799
800 fn draw_minimap(
801 &self,
802 buffer: &mut TheRGBABuffer,
803 project: &Project,
804 ctx: &mut TheContext,
805 server_ctx: &ServerContext,
806 ) -> bool {
807 buffer.fill(BLACK);
808
809 let display_ctx = if self.anim_preview {
811 let frame_count = server_ctx.editing_ctx.get_frame_count(project);
812 if frame_count > 0 {
813 let frame = server_ctx.animation_counter % frame_count;
814 server_ctx.editing_ctx.with_frame(frame)
815 } else {
816 server_ctx.editing_ctx
817 }
818 } else {
819 server_ctx.editing_ctx
820 };
821
822 if let Some(texture) = project.get_editing_texture(&display_ctx) {
823 let stride: usize = buffer.stride();
824
825 let src_pixels = &texture.data;
826 let src_w = texture.width as f32;
827 let src_h = texture.height as f32;
828
829 let dim = buffer.dim();
830 let dst_w = dim.width as f32;
831 let dst_h = dim.height as f32;
832
833 let scale = (dst_w / src_w).min(dst_h / src_h);
834 let draw_w = src_w * scale;
835 let draw_h = src_h * scale;
836
837 let offset_x = ((dst_w - draw_w) * 0.5).round() as usize;
838 let offset_y = ((dst_h - draw_h) * 0.5).round() as usize;
839
840 let dst_rect = (
841 offset_x,
842 offset_y,
843 draw_w.round() as usize,
844 draw_h.round() as usize,
845 );
846
847 ctx.draw.blend_scale_chunk(
848 buffer.pixels_mut(),
849 &dst_rect,
850 stride,
851 src_pixels,
852 &(src_w as usize, src_h as usize),
853 );
854
855 return true;
856 }
857 false
858 }
859
860 fn supports_minimap_animation(&self) -> bool {
861 true
862 }
863}
864
865impl TilesEditorDock {
866 fn apply_avatar_anchor_at(
867 &mut self,
868 coord: Vec2<i32>,
869 project: &mut Project,
870 ctx: &mut TheContext,
871 server_ctx: &ServerContext,
872 ) -> bool {
873 let editing_ctx = server_ctx.editing_ctx;
874 let Some(before) = project.get_editing_avatar_frame(&editing_ctx) else {
875 return false;
876 };
877 let before_main = before.weapon_main_anchor;
878 let before_off = before.weapon_off_anchor;
879
880 let clicked = Some((coord.x as i16, coord.y as i16));
881 if let Some(frame) = project.get_editing_avatar_frame_mut(&editing_ctx) {
882 match server_ctx.avatar_anchor_slot {
883 AvatarAnchorEditSlot::Main => {
884 if frame.weapon_main_anchor == clicked {
885 frame.weapon_main_anchor = None;
886 ctx.ui.send(TheEvent::SetStatusText(
887 TheId::empty(),
888 fl!("status_avatar_anchor_clear_main"),
889 ));
890 } else {
891 frame.weapon_main_anchor = clicked;
892 ctx.ui.send(TheEvent::SetStatusText(
893 TheId::empty(),
894 fl!("status_avatar_anchor_set_main"),
895 ));
896 }
897 }
898 AvatarAnchorEditSlot::Off => {
899 if frame.weapon_off_anchor == clicked {
900 frame.weapon_off_anchor = None;
901 ctx.ui.send(TheEvent::SetStatusText(
902 TheId::empty(),
903 fl!("status_avatar_anchor_clear_off"),
904 ));
905 } else {
906 frame.weapon_off_anchor = clicked;
907 ctx.ui.send(TheEvent::SetStatusText(
908 TheId::empty(),
909 fl!("status_avatar_anchor_set_off"),
910 ));
911 }
912 }
913 AvatarAnchorEditSlot::None => return false,
914 }
915
916 let after_main = frame.weapon_main_anchor;
917 let after_off = frame.weapon_off_anchor;
918 if before_main != after_main || before_off != after_off {
919 let atom = TileEditorUndoAtom::AvatarAnchorEdit(
920 editing_ctx,
921 before_main,
922 before_off,
923 after_main,
924 after_off,
925 );
926 self.add_undo(atom, ctx);
927 ctx.ui.send(TheEvent::Custom(
928 TheId::named("Editing Texture Updated"),
929 TheValue::Empty,
930 ));
931 return true;
932 }
933 }
934 false
935 }
936
937 fn sync_anchor_overlay(
938 &mut self,
939 project: &Project,
940 ui: &mut TheUI,
941 ctx: &mut TheContext,
942 server_ctx: &ServerContext,
943 ) {
944 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout")
945 && let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view()
946 {
947 let points =
948 if let Some(frame) = project.get_editing_avatar_frame(&server_ctx.editing_ctx) {
949 let mut p = vec![];
950 if let Some((x, y)) = frame.weapon_main_anchor {
951 p.push((Vec2::new(x as i32, y as i32), [255, 80, 80, 255]));
952 }
953 if let Some((x, y)) = frame.weapon_off_anchor {
954 p.push((Vec2::new(x as i32, y as i32), [80, 200, 255, 255]));
955 }
956 p
957 } else {
958 vec![]
959 };
960 rgba_view.set_anchor_points(points);
961 editor.relayout(ctx);
962 }
963 }
964
965 fn apply_flip(
966 &mut self,
967 horizontal: bool,
968 project: &mut Project,
969 ui: &mut TheUI,
970 ctx: &mut TheContext,
971 server_ctx: &mut ServerContext,
972 ) -> bool {
973 if self.paste_preview_texture.is_some() {
974 return false;
975 }
976
977 let selection = if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
978 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
979 rgba_view.selection()
980 } else {
981 FxHashSet::default()
982 }
983 } else {
984 FxHashSet::default()
985 };
986
987 let editing_ctx = server_ctx.editing_ctx;
988 let before = project.get_editing_texture(&editing_ctx).cloned();
989 let Some(texture) = project.get_editing_texture_mut(&editing_ctx) else {
990 return false;
991 };
992 let Some(before) = before else {
993 return false;
994 };
995
996 let mut after_data = texture.data.clone();
997 let w = texture.width as i32;
998 let h = texture.height as i32;
999
1000 if selection.is_empty() {
1001 for y in 0..h {
1002 for x in 0..w {
1003 let sx = if horizontal { w - 1 - x } else { x };
1004 let sy = if horizontal { y } else { h - 1 - y };
1005 let src_i = ((sy as usize) * texture.width + (sx as usize)) * 4;
1006 let dst_i = ((y as usize) * texture.width + (x as usize)) * 4;
1007 after_data[dst_i..dst_i + 4].copy_from_slice(&texture.data[src_i..src_i + 4]);
1008 }
1009 }
1010 } else {
1011 let min_x = selection.iter().map(|(x, _)| *x).min().unwrap_or(0);
1012 let max_x = selection.iter().map(|(x, _)| *x).max().unwrap_or(0);
1013 let min_y = selection.iter().map(|(_, y)| *y).min().unwrap_or(0);
1014 let max_y = selection.iter().map(|(_, y)| *y).max().unwrap_or(0);
1015
1016 for (x, y) in &selection {
1017 let sx = if horizontal { min_x + (max_x - *x) } else { *x };
1018 let sy = if horizontal { *y } else { min_y + (max_y - *y) };
1019 if sx >= 0
1020 && sy >= 0
1021 && sx < w
1022 && sy < h
1023 && selection.contains(&(sx, sy))
1024 && *x >= 0
1025 && *y >= 0
1026 && *x < w
1027 && *y < h
1028 {
1029 let src_i = ((sy as usize) * texture.width + (sx as usize)) * 4;
1030 let dst_i = ((*y as usize) * texture.width + (*x as usize)) * 4;
1031 after_data[dst_i..dst_i + 4].copy_from_slice(&texture.data[src_i..src_i + 4]);
1032 }
1033 }
1034 }
1035
1036 if after_data == texture.data {
1037 return false;
1038 }
1039
1040 texture.data = after_data;
1041 texture.generate_normals(true);
1042
1043 let after = texture.clone();
1044 let atom = TileEditorUndoAtom::TextureEdit(editing_ctx, before, after);
1045 self.add_undo(atom, ctx);
1046
1047 match editing_ctx {
1048 PixelEditingContext::Tile(tile_id, _) => {
1049 ctx.ui.send(TheEvent::Custom(
1050 TheId::named("Tile Updated"),
1051 TheValue::Id(tile_id),
1052 ));
1053 ctx.ui.send(TheEvent::Custom(
1054 TheId::named("Update Tilepicker"),
1055 TheValue::Empty,
1056 ));
1057 }
1058 PixelEditingContext::AvatarFrame(..) => {
1059 ctx.ui.send(TheEvent::Custom(
1060 TheId::named("Editing Texture Updated"),
1061 TheValue::Empty,
1062 ));
1063 }
1064 PixelEditingContext::None => {}
1065 }
1066 true
1067 }
1068
1069 pub fn switch_to_tile(
1071 &mut self,
1072 tile: &rusterix::Tile,
1073 ctx: &mut TheContext,
1074 server_ctx: &mut ServerContext,
1075 ) {
1076 self.current_tile_id = Some(tile.id);
1077 self.current_undo_key = Some(tile.id);
1078
1079 if server_ctx.curr_tile_frame_index >= tile.textures.len() {
1081 server_ctx.curr_tile_frame_index = 0;
1082 }
1083
1084 server_ctx.editing_ctx =
1085 PixelEditingContext::Tile(tile.id, server_ctx.curr_tile_frame_index);
1086
1087 self.set_undo_state_to_ui(ctx);
1088 }
1089
1090 pub fn set_frame_index(
1092 &mut self,
1093 index: usize,
1094 project: &Project,
1095 ui: &mut TheUI,
1096 ctx: &mut TheContext,
1097 server_ctx: &mut ServerContext,
1098 ) {
1099 if let Some(tile_id) = self.current_tile_id {
1101 if let Some(tile) = project.tiles.get(&tile_id) {
1102 if index < tile.textures.len() {
1103 server_ctx.curr_tile_frame_index = index;
1104 server_ctx.editing_ctx = PixelEditingContext::Tile(tile_id, index);
1105
1106 if let Some(tree_layout) = ui.get_tree_layout("Tile Editor Tree") {
1108 if let Some(tile_node) = tree_layout.get_node_by_id_mut(&self.tile_node) {
1109 if let Some(widget) = tile_node.widgets[2].as_tree_icons() {
1110 widget.set_selected_index(Some(index));
1111 }
1112 }
1113 }
1114
1115 self.update_editor_display(tile, ui, ctx, server_ctx);
1117 self.sync_anchor_overlay(project, ui, ctx, server_ctx);
1118 }
1119 }
1120 }
1121 }
1122
1123 fn update_editor_display(
1125 &mut self,
1126 tile: &rusterix::Tile,
1127 ui: &mut TheUI,
1128 ctx: &mut TheContext,
1129 server_ctx: &mut ServerContext,
1130 ) {
1131 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
1132 let view_width = editor.dim().width - 16;
1133 let view_height = editor.dim().height - 16;
1134
1135 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
1136 let frame_index = server_ctx
1137 .curr_tile_frame_index
1138 .min(tile.textures.len().saturating_sub(1));
1139
1140 if frame_index < tile.textures.len() {
1141 let buffer = tile.textures[frame_index].to_rgba();
1142 let icon_width = tile.textures[frame_index].width;
1143 let icon_height = tile.textures[frame_index].height;
1144
1145 self.zoom = (view_width as f32 / icon_width as f32)
1146 .min(view_height as f32 / icon_height as f32);
1147
1148 rgba_view.set_buffer(buffer);
1149 editor.set_zoom(self.zoom);
1150 editor.relayout(ctx);
1151 }
1152 }
1153 }
1154 }
1155
1156 pub fn update_frame_icons(&self, tile: &rusterix::Tile, ui: &mut TheUI) {
1158 if let Some(tree_layout) = ui.get_tree_layout("Tile Editor Tree") {
1159 if let Some(tile_node) = tree_layout.get_node_by_id_mut(&self.tile_node) {
1160 if let Some(widget) = tile_node.widgets[2].as_tree_icons() {
1161 for (index, texture) in tile.textures.iter().enumerate() {
1163 widget.set_icon(index, texture.to_rgba());
1164 }
1165 }
1166 }
1167 }
1168 }
1169
1170 pub fn add_undo(&mut self, atom: TileEditorUndoAtom, ctx: &mut TheContext) {
1172 let key = match &atom {
1173 TileEditorUndoAtom::TileEdit(tile_id, _, _) => Some(*tile_id),
1174 TileEditorUndoAtom::TextureEdit(editing_ctx, _, _) => match editing_ctx {
1175 PixelEditingContext::Tile(tile_id, _) => Some(*tile_id),
1176 PixelEditingContext::AvatarFrame(avatar_id, _, _, _) => Some(*avatar_id),
1177 PixelEditingContext::None => None,
1178 },
1179 TileEditorUndoAtom::AvatarAnchorEdit(editing_ctx, _, _, _, _) => match editing_ctx {
1180 PixelEditingContext::Tile(tile_id, _) => Some(*tile_id),
1181 PixelEditingContext::AvatarFrame(avatar_id, _, _, _) => Some(*avatar_id),
1182 PixelEditingContext::None => None,
1183 },
1184 };
1185 if let Some(key) = key {
1186 let undo = self
1187 .tile_undos
1188 .entry(key)
1189 .or_insert_with(TileEditorUndo::new);
1190 undo.add(atom);
1191 undo.truncate_to_limit(self.max_undo);
1192 self.set_undo_state_to_ui(ctx);
1193 }
1194 }
1195
1196 fn sync_paste_preview(&mut self, ui: &mut TheUI, ctx: &mut TheContext) {
1197 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout")
1198 && let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view()
1199 {
1200 if let (Some(texture), Some(pos)) =
1201 (&self.paste_preview_texture, self.paste_preview_pos)
1202 {
1203 let top_left = Self::paste_top_left_from_center(pos, texture);
1204 rgba_view.set_paste_preview(Some((texture.to_rgba(), top_left)));
1205 } else {
1206 rgba_view.set_paste_preview(None);
1207 }
1208 editor.relayout(ctx);
1209 }
1210 }
1211
1212 fn clear_paste_preview(&mut self, ui: &mut TheUI, ctx: &mut TheContext) {
1213 self.paste_preview_texture = None;
1214 self.paste_preview_pos = None;
1215 self.sync_paste_preview(ui, ctx);
1216 }
1217
1218 fn apply_paste_at_preview(
1219 &mut self,
1220 project: &mut Project,
1221 ui: &mut TheUI,
1222 ctx: &mut TheContext,
1223 server_ctx: &mut ServerContext,
1224 ) -> bool {
1225 let Some(pasted) = self.paste_preview_texture.clone() else {
1226 return false;
1227 };
1228 let Some(anchor) = self.paste_preview_pos else {
1229 return false;
1230 };
1231 let paste_top_left = Self::paste_top_left_from_center(anchor, &pasted);
1232
1233 let selection = if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
1234 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
1235 rgba_view.selection()
1236 } else {
1237 FxHashSet::default()
1238 }
1239 } else {
1240 FxHashSet::default()
1241 };
1242
1243 let editing_ctx = server_ctx.editing_ctx;
1244 let before = project.get_editing_texture(&editing_ctx).cloned();
1245 if let Some(texture) = project.get_editing_texture_mut(&editing_ctx) {
1246 let before = if let Some(before) = before {
1247 before
1248 } else {
1249 return false;
1250 };
1251 let mut changed = false;
1252
1253 if selection.is_empty() {
1254 for sy in 0..pasted.height {
1255 for sx in 0..pasted.width {
1256 let tx = paste_top_left.x + sx as i32;
1257 let ty = paste_top_left.y + sy as i32;
1258 if tx >= 0
1259 && ty >= 0
1260 && (tx as usize) < texture.width
1261 && (ty as usize) < texture.height
1262 {
1263 let src_i = (sy * pasted.width + sx) * 4;
1264 let dst_i = ((ty as usize) * texture.width + (tx as usize)) * 4;
1265 texture.data[dst_i..dst_i + 4]
1266 .copy_from_slice(&pasted.data[src_i..src_i + 4]);
1267 changed = true;
1268 }
1269 }
1270 }
1271 } else {
1272 for sy in 0..pasted.height {
1273 for sx in 0..pasted.width {
1274 let tx = paste_top_left.x + sx as i32;
1275 let ty = paste_top_left.y + sy as i32;
1276 if tx >= 0
1277 && ty >= 0
1278 && (tx as usize) < texture.width
1279 && (ty as usize) < texture.height
1280 && selection.contains(&(tx, ty))
1281 {
1282 let src_i = (sy * pasted.width + sx) * 4;
1283 let dst_i = ((ty as usize) * texture.width + (tx as usize)) * 4;
1284 texture.data[dst_i..dst_i + 4]
1285 .copy_from_slice(&pasted.data[src_i..src_i + 4]);
1286 changed = true;
1287 }
1288 }
1289 }
1290 }
1291
1292 if !changed {
1293 return false;
1294 }
1295
1296 texture.generate_normals(true);
1297 let after = texture.clone();
1298 let atom = TileEditorUndoAtom::TextureEdit(editing_ctx, before, after);
1299 self.add_undo(atom, ctx);
1300
1301 match editing_ctx {
1302 PixelEditingContext::Tile(tile_id, _) => {
1303 ctx.ui.send(TheEvent::Custom(
1304 TheId::named("Tile Updated"),
1305 TheValue::Id(tile_id),
1306 ));
1307 ctx.ui.send(TheEvent::Custom(
1308 TheId::named("Update Tilepicker"),
1309 TheValue::Empty,
1310 ));
1311 }
1312 PixelEditingContext::AvatarFrame(..) => {
1313 ctx.ui.send(TheEvent::Custom(
1314 TheId::named("Editing Texture Updated"),
1315 TheValue::Empty,
1316 ));
1317 }
1318 PixelEditingContext::None => {}
1319 }
1320 return true;
1321 }
1322 false
1323 }
1324
1325 fn clear_current_selection(
1326 &mut self,
1327 project: &mut Project,
1328 ui: &mut TheUI,
1329 ctx: &mut TheContext,
1330 server_ctx: &mut ServerContext,
1331 ) -> bool {
1332 if server_ctx.editing_ctx == PixelEditingContext::None {
1333 return false;
1334 }
1335
1336 let selection = if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
1337 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
1338 rgba_view.selection()
1339 } else {
1340 FxHashSet::default()
1341 }
1342 } else {
1343 FxHashSet::default()
1344 };
1345
1346 if selection.is_empty() {
1347 return false;
1348 }
1349
1350 let editing_ctx = server_ctx.editing_ctx;
1351 let before = project.get_editing_texture(&editing_ctx).cloned();
1352 if let Some(texture) = project.get_editing_texture_mut(&editing_ctx) {
1353 let before = if let Some(before) = before {
1354 before
1355 } else {
1356 return false;
1357 };
1358 let mut changed = false;
1359 for (x, y) in selection {
1360 if x >= 0 && y >= 0 && (x as usize) < texture.width && (y as usize) < texture.height
1361 {
1362 let i = ((y as usize) * texture.width + (x as usize)) * 4;
1363 if texture.data[i..i + 4] != [0, 0, 0, 0] {
1364 texture.data[i..i + 4].copy_from_slice(&[0, 0, 0, 0]);
1365 changed = true;
1366 }
1367 }
1368 }
1369 if changed {
1370 texture.generate_normals(true);
1371 let after = texture.clone();
1372 let atom = TileEditorUndoAtom::TextureEdit(editing_ctx, before, after);
1373 self.add_undo(atom, ctx);
1374
1375 match editing_ctx {
1376 PixelEditingContext::Tile(tile_id, _) => {
1377 ctx.ui.send(TheEvent::Custom(
1378 TheId::named("Tile Updated"),
1379 TheValue::Id(tile_id),
1380 ));
1381 ctx.ui.send(TheEvent::Custom(
1382 TheId::named("Update Tilepicker"),
1383 TheValue::Empty,
1384 ));
1385 }
1386 PixelEditingContext::AvatarFrame(..) => {
1387 ctx.ui.send(TheEvent::Custom(
1388 TheId::named("Editing Texture Updated"),
1389 TheValue::Empty,
1390 ));
1391 }
1392 PixelEditingContext::None => {}
1393 }
1394
1395 return true;
1396 }
1397 }
1398 false
1399 }
1400
1401 #[inline]
1402 fn paste_top_left_from_center(anchor: Vec2<i32>, pasted: &rusterix::Texture) -> Vec2<i32> {
1403 Vec2::new(
1404 anchor.x - pasted.width as i32 / 2,
1405 anchor.y - pasted.height as i32 / 2,
1406 )
1407 }
1408
1409 pub fn set_tile(
1411 &mut self,
1412 tile: &rusterix::Tile,
1413 ui: &mut TheUI,
1414 ctx: &mut TheContext,
1415 server_ctx: &mut ServerContext,
1416 update_only: bool,
1417 ) {
1418 if !update_only {
1420 self.switch_to_tile(tile, ctx, server_ctx);
1421
1422 if let Some(tree_layout) = ui.get_tree_layout("Tile Editor Tree") {
1423 if let Some(tile_node) = tree_layout.get_node_by_id_mut(&self.tile_node) {
1424 if let Some(widget) = tile_node.widgets[0].as_tree_item() {
1426 if let Some(embedded) = widget.embedded_widget_mut() {
1427 if !tile.is_empty() {
1428 embedded.set_value(TheValue::Int(tile.textures[0].width as i32));
1429 }
1430 }
1431 }
1432 if let Some(widget) = tile_node.widgets[1].as_tree_item() {
1434 if let Some(embedded) = widget.embedded_widget_mut() {
1435 if !tile.is_empty() {
1436 embedded.set_value(TheValue::Int(tile.textures.len() as i32));
1437 }
1438 }
1439 }
1440 if let Some(widget) = tile_node.widgets[2].as_tree_icons() {
1442 widget.set_icon_count(tile.textures.len());
1443 for (index, texture) in tile.textures.iter().enumerate() {
1444 widget.set_icon(index, texture.to_rgba());
1445 }
1446 }
1447 }
1448 }
1449 }
1450
1451 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
1452 let view_width = editor.dim().width - 16;
1453 let view_height = editor.dim().height - 16;
1454
1455 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
1456 let frame_index = server_ctx
1458 .curr_tile_frame_index
1459 .min(tile.textures.len().saturating_sub(1));
1460
1461 if frame_index < tile.textures.len() {
1462 let buffer = tile.textures[frame_index].to_rgba();
1463
1464 if !update_only {
1465 rgba_view.set_grid(Some(1));
1466 rgba_view.set_dont_show_grid(!self.show_grid);
1467
1468 let icon_width = tile.textures[frame_index].width;
1469 let icon_height = tile.textures[frame_index].height;
1470
1471 self.zoom = (view_width as f32 / icon_width as f32)
1472 .min(view_height as f32 / icon_height as f32);
1473 }
1474 rgba_view.set_buffer(buffer);
1475 }
1476 }
1477 if !update_only {
1478 editor.set_zoom(self.zoom);
1479 editor.relayout(ctx);
1480 }
1481 }
1482 }
1483
1484 pub fn editing_context_changed(
1487 &mut self,
1488 ui: &mut TheUI,
1489 ctx: &mut TheContext,
1490 project: &Project,
1491 server_ctx: &mut ServerContext,
1492 ) {
1493 if self.paste_preview_texture.is_some() {
1494 self.clear_paste_preview(ui, ctx);
1495 }
1496 match server_ctx.editing_ctx {
1497 PixelEditingContext::Tile(tile_id, _) => {
1498 server_ctx.avatar_anchor_slot = AvatarAnchorEditSlot::None;
1499 if let Some(tile) = project.tiles.get(&tile_id) {
1500 self.set_tile(tile, ui, ctx, server_ctx, false);
1501 if let Some(stack) = ui.get_stack_layout("Pixel Editor Stack Layout") {
1502 stack.set_index(0);
1503 }
1504 }
1505 }
1506 PixelEditingContext::AvatarFrame(..) => {
1507 self.set_undo_key_from_context(&server_ctx.editing_ctx);
1508 self.refresh_from_editing_context(project, ui, ctx, server_ctx);
1509 if let Some(stack) = ui.get_stack_layout("Pixel Editor Stack Layout") {
1510 stack.set_index(1);
1511 }
1512 }
1513 PixelEditingContext::None => {
1514 server_ctx.avatar_anchor_slot = AvatarAnchorEditSlot::None;
1515 if let Some(tile_id) = server_ctx.curr_tile_id {
1516 if let Some(tile) = project.tiles.get(&tile_id) {
1517 self.set_tile(tile, ui, ctx, server_ctx, false);
1518 }
1519 }
1520 }
1521 }
1522 self.sync_anchor_overlay(project, ui, ctx, server_ctx);
1523 }
1524
1525 pub fn set_undo_key_from_context(&mut self, editing_ctx: &PixelEditingContext) {
1527 self.current_undo_key = match editing_ctx {
1528 PixelEditingContext::None => None,
1529 PixelEditingContext::Tile(tile_id, _) => Some(*tile_id),
1530 PixelEditingContext::AvatarFrame(avatar_id, _, _, _) => Some(*avatar_id),
1531 };
1532 }
1533
1534 pub fn refresh_from_editing_context(
1536 &mut self,
1537 project: &Project,
1538 ui: &mut TheUI,
1539 ctx: &mut TheContext,
1540 server_ctx: &mut ServerContext,
1541 ) {
1542 if let Some(texture) = project.get_editing_texture(&server_ctx.editing_ctx) {
1543 self.set_editing_texture(texture, ui, ctx);
1544 }
1545 self.sync_anchor_overlay(project, ui, ctx, server_ctx);
1546 }
1547
1548 pub fn set_editing_texture(
1550 &mut self,
1551 texture: &rusterix::Texture,
1552 ui: &mut TheUI,
1553 ctx: &mut TheContext,
1554 ) {
1555 if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout") {
1556 let view_width = editor.dim().width - 16;
1557 let view_height = editor.dim().height - 16;
1558
1559 if let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view() {
1560 let buffer = texture.to_rgba();
1561 let icon_width = texture.width;
1562 let icon_height = texture.height;
1563
1564 self.zoom = (view_width as f32 / icon_width as f32)
1565 .min(view_height as f32 / icon_height as f32);
1566
1567 rgba_view.set_grid(Some(1));
1568 rgba_view.set_dont_show_grid(!self.show_grid);
1569 rgba_view.set_buffer(buffer);
1570 editor.set_zoom(self.zoom);
1571 editor.relayout(ctx);
1572 }
1573 }
1574 }
1575}