Skip to main content

rustapi/editor_tools/
tile_draw.rs

1use crate::docks::tiles_editor_undo::TileEditorUndoAtom;
2use crate::prelude::*;
3
4pub struct TileDrawTool {
5    id: TheId,
6    /// For tile editing: snapshot of the entire tile before the stroke.
7    before_tile: Option<rusterix::Tile>,
8    /// For non-tile editing (avatar frames, etc.): snapshot of the texture + context.
9    before_snapshot: Option<(PixelEditingContext, rusterix::Texture)>,
10    changed: bool,
11}
12
13impl EditorTool for TileDrawTool {
14    fn new() -> Self
15    where
16        Self: Sized,
17    {
18        Self {
19            id: TheId::named_with_id("Tile Draw Tool", Uuid::new_v4()),
20            before_tile: None,
21            before_snapshot: None,
22            changed: false,
23        }
24    }
25
26    fn id(&self) -> TheId {
27        self.id.clone()
28    }
29
30    fn info(&self) -> String {
31        "Draw Tool (D). Click and drag to draw pixels with the selected palette color.".to_string()
32    }
33
34    fn icon_name(&self) -> String {
35        "paint-brush".to_string()
36    }
37
38    fn rgba_view_mode(&self) -> Option<TheRGBAViewMode> {
39        Some(TheRGBAViewMode::TileEditor)
40    }
41
42    fn accel(&self) -> Option<char> {
43        Some('D')
44    }
45
46    fn handle_event(
47        &mut self,
48        event: &TheEvent,
49        ui: &mut TheUI,
50        ctx: &mut TheContext,
51        project: &mut Project,
52        server_ctx: &mut ServerContext,
53    ) -> bool {
54        let mut redraw = false;
55
56        match event {
57            TheEvent::TileEditorClicked(id, coord) => {
58                if id.name == "Tile Editor Dock RGBA Layout View" {
59                    // Snapshot before we start drawing
60                    match server_ctx.editing_ctx {
61                        PixelEditingContext::Tile(tile_id, _) => {
62                            if let Some(tile) = project.tiles.get(&tile_id) {
63                                self.before_tile = Some(tile.clone());
64                            }
65                        }
66                        _ => {
67                            if let Some(texture) =
68                                project.get_editing_texture(&server_ctx.editing_ctx)
69                            {
70                                self.before_snapshot =
71                                    Some((server_ctx.editing_ctx, texture.clone()));
72                            }
73                        }
74                    }
75
76                    self.draw_pixel(*coord, ui, ctx, project, server_ctx);
77                    redraw = true;
78                }
79            }
80            TheEvent::TileEditorDragged(id, coord) => {
81                if id.name == "Tile Editor Dock RGBA Layout View" {
82                    self.draw_pixel(*coord, ui, ctx, project, server_ctx);
83                    redraw = true;
84                }
85            }
86            TheEvent::TileEditorUp(_) => {
87                if self.changed {
88                    // For tiles, update the tile picker when the stroke finishes
89                    if matches!(server_ctx.editing_ctx, PixelEditingContext::Tile(..)) {
90                        ctx.ui.send(TheEvent::Custom(
91                            TheId::named("Update Tilepicker"),
92                            TheValue::Empty,
93                        ));
94                    }
95                    ctx.ui.send(TheEvent::Custom(
96                        TheId::named("Tile Editor Undo Available"),
97                        TheValue::Empty,
98                    ));
99                    self.changed = false;
100                }
101            }
102            _ => {}
103        }
104
105        redraw
106    }
107
108    fn get_undo_atom(&mut self, project: &Project) -> Option<Box<dyn std::any::Any>> {
109        // For tiles, use the TileEdit atom (works with tile picker and map even when editor closed)
110        if let Some(before) = self.before_tile.take() {
111            if let Some(tile) = project.tiles.get(&before.id) {
112                if !tile.textures.is_empty() {
113                    let after = tile.clone();
114                    let atom = TileEditorUndoAtom::TileEdit(before.id, before, after);
115                    return Some(Box::new(atom));
116                }
117            }
118            return None;
119        }
120
121        // For non-tile contexts, use the generic TextureEdit atom
122        if let Some((editing_ctx, before)) = self.before_snapshot.take() {
123            if let Some(after) = project.get_editing_texture(&editing_ctx) {
124                let atom = TileEditorUndoAtom::TextureEdit(editing_ctx, before, after.clone());
125                return Some(Box::new(atom));
126            }
127        }
128        None
129    }
130}
131
132impl TileDrawTool {
133    fn draw_pixel(
134        &mut self,
135        pos: Vec2<i32>,
136        ui: &mut TheUI,
137        ctx: &mut TheContext,
138        project: &mut Project,
139        server_ctx: &mut ServerContext,
140    ) {
141        let editing_ctx = server_ctx.editing_ctx;
142
143        // Shift clears the pixel, otherwise use the context draw color
144        let color_array = if ui.shift {
145            Some([0, 0, 0, 0])
146        } else {
147            editing_ctx.get_draw_color(
148                &project.palette,
149                server_ctx.palette_opacity,
150                server_ctx.body_marker_color,
151            )
152        };
153
154        if let Some(color_array) = color_array {
155            if matches!(editing_ctx, PixelEditingContext::AvatarFrame(..))
156                && server_ctx.avatar_anchor_slot != AvatarAnchorEditSlot::None
157            {
158                return;
159            }
160            if let Some(editor) = ui.get_rgba_layout("Tile Editor Dock RGBA Layout")
161                && let Some(rgba_view) = editor.rgba_view_mut().as_rgba_view()
162            {
163                if rgba_view.has_paste_preview() {
164                    return;
165                }
166                let selection = rgba_view.selection();
167                if !selection.is_empty() && !selection.contains(&(pos.x, pos.y)) {
168                    return;
169                }
170            }
171
172            if let Some(texture) = project.get_editing_texture_mut(&editing_ctx) {
173                let width = texture.width as i32;
174                let height = texture.height as i32;
175
176                if pos.x >= 0 && pos.x < width && pos.y >= 0 && pos.y < height {
177                    texture.set_pixel(pos.x as u32, pos.y as u32, color_array);
178                    texture.generate_normals(true);
179
180                    // Send context-appropriate update event
181                    match editing_ctx {
182                        PixelEditingContext::Tile(tile_id, _) => {
183                            ctx.ui.send(TheEvent::Custom(
184                                TheId::named("Tile Updated"),
185                                TheValue::Id(tile_id),
186                            ));
187                        }
188                        PixelEditingContext::AvatarFrame(..) => {
189                            ctx.ui.send(TheEvent::Custom(
190                                TheId::named("Editing Texture Updated"),
191                                TheValue::Empty,
192                            ));
193                        }
194                        PixelEditingContext::None => {}
195                    }
196
197                    self.changed = true;
198                }
199            }
200        }
201    }
202}