Skip to main content

rustapi/docks/
tiles_editor.rs

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    // Per-context undo stacks (keyed by tile_id for tiles, avatar_id for avatar frames)
14    tile_undos: FxHashMap<Uuid, TileEditorUndo>,
15    current_tile_id: Option<Uuid>,
16    /// The current undo key — derived from the editing context.
17    current_undo_key: Option<Uuid>,
18    max_undo: usize,
19
20    /// When true, the minimap cycles through animation frames.
21    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_grid(Some(1));
56            // rgba_view.set_grid_color([20, 20, 20, 255]);
57            // rgba_view.set_dont_show_grid(true);
58            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        // Tree
73
74        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        // Tile
80        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        // Palette
109
110        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        // let mut item = TheTreeIcons::new(TheId::named("Palette Item"));
124        // item.set_icon_count(256);
125        // item.set_icons_per_row(14);
126        // item.set_selected_index(Some(0));
127
128        // palette_node.add_widget(Box::new(item));
129        root.add_child(palette_node);
130
131        // Grid
132        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        //
147
148        palette_canvas.set_layout(palette_tree_layout);
149
150        stack_layout.add_canvas(palette_canvas);
151
152        // Avatar
153
154        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        // •	Skin Light – rgb(255, 0, 255)
166        // •	Skin Dark – rgb(200, 0, 200)
167        // •	Torso / Chest – rgb(0, 0, 255)
168        // •	Arms / Sleeves – rgb(0, 120, 255)
169        // •	Legs / Pants – rgb(0, 255, 0)
170        // •	Hair – rgb(255, 255, 0)
171        // •	Eyes / Face Detail – rgb(0, 255, 255)
172        // •	Hands – rgb(255, 128, 0)
173        // •	Feet – rgb(255, 80, 0)
174
175        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        // let mut item = TheTreeItem::new(TheId::named("Body: Extra"));
234        // item.set_text(fl!("extra"));
235        // item.set_background_color(TheColor::from_u8_array_3([255, 0, 0]));
236        // body_markers_node.add_widget(Box::new(item));
237
238        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                        // Update the current frame
325                        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                                // Update the frame icon
329                                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                // The Size of the Tile has been edited
358                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                // The frame count of the Tile has been edited
380                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                // The palette opacity has been edited
401                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                    // New frame index selected - update the editor display
410                    self.set_frame_index(*index as usize, project, ui, ctx, server_ctx);
411                }
412                // else if id.name == "Palette Item" {
413                //     project.palette.current_index = *index as u16;
414                // }
415            }
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                    // Copy selected pixels first.
585                    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                            // Convert RGBA image data to a texture
634                            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        // Check if any tile has changes (index >= 0, meaning not fully undone)
728        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        // No tile selected or no undo stack
786        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        // Determine which frame to display
810        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    /// Switch to a different tile and update undo button states
1070    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        // Verify frame index is valid for the new tile
1080        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    /// Set the current frame/texture index
1091    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        // Verify the index is valid for current tile
1100        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                    // Update the TreeIcons selection
1107                    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                    // Refresh the display with the new frame
1116                    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    /// Update just the editor display (for when frame index changes)
1124    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    /// Update the frame icons in the tree (call after editing a texture)
1157    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                    // Update all frame icons
1162                    for (index, texture) in tile.textures.iter().enumerate() {
1163                        widget.set_icon(index, texture.to_rgba());
1164                    }
1165                }
1166            }
1167        }
1168    }
1169
1170    /// Add an undo atom to the appropriate undo stack (keyed by context)
1171    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    /// Set the tile for the editor.
1410    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        // Switch to this tile's undo stack
1419        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                    // Set the tile size
1425                    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                    // Set the frames editor
1433                    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                    // Set the frames editor
1441                    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                // Use current frame index, ensure it's valid
1457                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    /// Called whenever the editing context changes (activate, tile picked, avatar frame selected).
1485    /// Use this to adjust UI elements based on the current PixelEditingContext.
1486    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    /// Set the undo key based on the current editing context.
1526    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    /// Refresh the editor display from the current editing context.
1535    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    /// Display the given texture in the editor.
1549    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}