added text input
This commit is contained in:
@@ -47,6 +47,13 @@ pub struct Window {
|
||||
pub span_width: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TextLabel {
|
||||
pub cell: (usize, usize),
|
||||
pub text: String,
|
||||
pub font_size: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WindowSide {
|
||||
Left,
|
||||
@@ -80,6 +87,7 @@ pub struct DungeonLayout {
|
||||
pub corridors: Vec<Corridor>,
|
||||
pub doors: Vec<Door>,
|
||||
pub windows: Vec<Window>,
|
||||
pub text_labels: Vec<TextLabel>,
|
||||
pub packed_rooms: bool,
|
||||
}
|
||||
|
||||
@@ -90,6 +98,7 @@ impl Default for DungeonLayout {
|
||||
corridors: Vec::new(),
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
packed_rooms: false,
|
||||
}
|
||||
}
|
||||
@@ -156,6 +165,7 @@ pub fn generate_layout(
|
||||
corridors,
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
packed_rooms: pack_rooms_without_corridors,
|
||||
};
|
||||
}
|
||||
@@ -178,6 +188,7 @@ pub fn generate_layout(
|
||||
corridors,
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
packed_rooms: pack_rooms_without_corridors,
|
||||
};
|
||||
}
|
||||
@@ -205,6 +216,7 @@ pub fn generate_layout(
|
||||
corridors,
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
packed_rooms: true,
|
||||
};
|
||||
apply_doors(&mut layout, seed, door_settings, cols, rows);
|
||||
@@ -261,6 +273,7 @@ pub fn generate_layout(
|
||||
corridors,
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
packed_rooms: false,
|
||||
};
|
||||
}
|
||||
@@ -314,6 +327,7 @@ pub fn generate_layout(
|
||||
corridors,
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
packed_rooms: false,
|
||||
};
|
||||
apply_doors(&mut layout, seed, door_settings, cols, rows);
|
||||
|
||||
+220
-11
@@ -51,10 +51,11 @@ struct DungeonApp {
|
||||
export_rx: Option<mpsc::Receiver<Result<std::path::PathBuf, String>>>,
|
||||
pending_delete: bool,
|
||||
add_corridor_drag: Option<AddCorridorDrag>,
|
||||
resize_state: Option<RoomResizeState>,
|
||||
resize_state: Option<ResizeState>,
|
||||
hover_room_idx: Option<usize>,
|
||||
hover_corridor_idx: Option<usize>,
|
||||
hover_door_idx: Option<usize>,
|
||||
hover_text_idx: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for DungeonApp {
|
||||
@@ -91,6 +92,7 @@ impl Default for DungeonApp {
|
||||
hover_room_idx: None,
|
||||
hover_corridor_idx: None,
|
||||
hover_door_idx: None,
|
||||
hover_text_idx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,11 +217,11 @@ impl eframe::App for DungeonApp {
|
||||
ui.add_space(8.0);
|
||||
ui.heading("Keybinds");
|
||||
ui.add_space(6.0);
|
||||
ui.label("Delete / Backspace: Remove hovered room or corridor");
|
||||
ui.label("Ctrl+Z: Undo");
|
||||
ui.label("Ctrl+Y: Redo");
|
||||
ui.label("Right click: Cancel add tool");
|
||||
ui.label("Right click + drag: Resize hovered room");
|
||||
ui.label("• Delete / Backspace: Remove hovered room or corridor");
|
||||
ui.label("• Ctrl+Z: Undo");
|
||||
ui.label("• Ctrl+Y: Redo");
|
||||
ui.label("• Right click: Cancel add tool");
|
||||
ui.label("• Right click + drag: Resize hovered room");
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
@@ -244,6 +246,7 @@ impl eframe::App for DungeonApp {
|
||||
&geometry,
|
||||
&self.layout,
|
||||
self.settings.colorblind_mode,
|
||||
self.hover_text_idx,
|
||||
);
|
||||
self.draw_add_overlay(&painter, &geometry);
|
||||
self.draw_resize_overlay(&painter, &geometry);
|
||||
@@ -284,6 +287,7 @@ impl DungeonApp {
|
||||
self.hover_room_idx = None;
|
||||
self.hover_corridor_idx = None;
|
||||
self.hover_door_idx = None;
|
||||
self.hover_text_idx = None;
|
||||
self.pending_delete = false;
|
||||
}
|
||||
|
||||
@@ -316,6 +320,7 @@ impl DungeonApp {
|
||||
self.hover_room_idx = None;
|
||||
self.hover_corridor_idx = None;
|
||||
self.hover_door_idx = None;
|
||||
self.hover_text_idx = None;
|
||||
self.pending_delete = false;
|
||||
}
|
||||
|
||||
@@ -355,6 +360,12 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
if response.drag_started()
|
||||
&& let Some(pointer_pos) = response.interact_pointer_pos()
|
||||
&& let Some(text_idx) = self.text_at_pointer(pointer_pos, geometry)
|
||||
{
|
||||
self.push_undo_snapshot();
|
||||
self.drag_state = Some(DragState::Text(TextDragState { text_idx }));
|
||||
} else if response.drag_started()
|
||||
&& let Some(pointer_pos) = response.interact_pointer_pos()
|
||||
&& let Some((room_idx, offset_x, offset_y)) =
|
||||
self.room_at_pointer(pointer_pos, geometry)
|
||||
@@ -379,6 +390,7 @@ impl DungeonApp {
|
||||
{
|
||||
match drag_state {
|
||||
DragState::Room(drag) => self.drag_room_to_pointer(drag, pointer_pos, geometry),
|
||||
DragState::Text(drag) => self.drag_text_to_pointer(drag, pointer_pos, geometry),
|
||||
DragState::Corridor(drag) => {
|
||||
self.drag_corridor_to_pointer(drag, pointer_pos, geometry)
|
||||
}
|
||||
@@ -450,6 +462,29 @@ impl DungeonApp {
|
||||
self.refresh_doors();
|
||||
}
|
||||
|
||||
fn drag_text_to_pointer(
|
||||
&mut self,
|
||||
drag: TextDragState,
|
||||
pointer_pos: egui::Pos2,
|
||||
geometry: &GridGeometry,
|
||||
) {
|
||||
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if drag.text_idx >= self.layout.text_labels.len() {
|
||||
self.drag_state = None;
|
||||
return;
|
||||
}
|
||||
|
||||
let target = (
|
||||
(grid_x.floor().max(0.0) as usize).min(self.settings.cols.saturating_sub(1)),
|
||||
(grid_y.floor().max(0.0) as usize).min(self.settings.rows.saturating_sub(1)),
|
||||
);
|
||||
|
||||
self.layout.text_labels[drag.text_idx].cell = target;
|
||||
}
|
||||
|
||||
// Recalculate corridor paths connected to a moved room.
|
||||
fn reroute_corridors_for_room(&mut self, room_idx: usize) {
|
||||
if self.layout.packed_rooms {
|
||||
@@ -621,6 +656,17 @@ impl DungeonApp {
|
||||
self.settings.add_tool = AddTool::None;
|
||||
}
|
||||
}
|
||||
AddTool::Text => {
|
||||
self.add_corridor_drag = None;
|
||||
if response.clicked()
|
||||
&& let Some(pointer_pos) = response.interact_pointer_pos()
|
||||
&& let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry)
|
||||
{
|
||||
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||
self.add_text_at_cell(cell);
|
||||
self.settings.add_tool = AddTool::None;
|
||||
}
|
||||
}
|
||||
AddTool::None => {
|
||||
self.add_corridor_drag = None;
|
||||
}
|
||||
@@ -720,12 +766,41 @@ impl DungeonApp {
|
||||
self.refresh_doors();
|
||||
}
|
||||
|
||||
fn add_text_at_cell(&mut self, cell: (usize, usize)) {
|
||||
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.settings.add_text_value.trim().to_string();
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.push_undo_snapshot();
|
||||
if let Some(existing) = self
|
||||
.layout
|
||||
.text_labels
|
||||
.iter_mut()
|
||||
.find(|label| label.cell == cell)
|
||||
{
|
||||
existing.text = text;
|
||||
return;
|
||||
}
|
||||
|
||||
self.layout.text_labels.push(layout::TextLabel {
|
||||
cell,
|
||||
text,
|
||||
font_size: 18,
|
||||
});
|
||||
}
|
||||
|
||||
// Update which room/corridor/door is currently hovered.
|
||||
fn update_hover_targets(&mut self, ctx: &egui::Context, geometry: &GridGeometry) {
|
||||
let Some(pointer_pos) = ctx.pointer_hover_pos() else {
|
||||
self.hover_room_idx = None;
|
||||
self.hover_corridor_idx = None;
|
||||
self.hover_door_idx = None;
|
||||
self.hover_text_idx = None;
|
||||
return;
|
||||
};
|
||||
if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) {
|
||||
@@ -738,10 +813,19 @@ impl DungeonApp {
|
||||
if self.hover_door_idx.is_some() {
|
||||
self.hover_room_idx = None;
|
||||
self.hover_corridor_idx = None;
|
||||
self.hover_text_idx = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.hover_text_idx = self.text_at_pointer(pointer_pos, geometry);
|
||||
if self.hover_text_idx.is_some() {
|
||||
self.hover_room_idx = None;
|
||||
self.hover_corridor_idx = None;
|
||||
self.hover_door_idx = None;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((room_idx, _, _)) = self.room_at_pointer(pointer_pos, geometry) {
|
||||
self.hover_room_idx = Some(room_idx);
|
||||
self.hover_corridor_idx = None;
|
||||
@@ -758,6 +842,15 @@ impl DungeonApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn text_at_pointer(&self, pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<usize> {
|
||||
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||
self.layout
|
||||
.text_labels
|
||||
.iter()
|
||||
.position(|label| label.cell == clicked)
|
||||
}
|
||||
|
||||
// Handle right-click resizing of rooms and return whether resizing is active.
|
||||
fn handle_resize(
|
||||
&mut self,
|
||||
@@ -771,10 +864,16 @@ impl DungeonApp {
|
||||
|
||||
if response.secondary_clicked() && self.resize_state.is_none() {
|
||||
if let Some(pointer_pos) = response.interact_pointer_pos() {
|
||||
if let Some(room_idx) = self.resize_room_at_pointer(pointer_pos, geometry) {
|
||||
if let Some(text_idx) = self.text_at_pointer(pointer_pos, geometry) {
|
||||
if let Some(state) = self.start_text_resize(text_idx) {
|
||||
self.push_undo_snapshot();
|
||||
self.resize_state = Some(ResizeState::Text(state));
|
||||
return true;
|
||||
}
|
||||
} else if let Some(room_idx) = self.resize_room_at_pointer(pointer_pos, geometry) {
|
||||
if let Some(state) = self.start_resize(room_idx, pointer_pos, geometry) {
|
||||
self.push_undo_snapshot();
|
||||
self.resize_state = Some(state);
|
||||
self.resize_state = Some(ResizeState::Room(state));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -785,7 +884,10 @@ impl DungeonApp {
|
||||
if let Some(pointer_pos) = response.interact_pointer_pos() {
|
||||
if let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) {
|
||||
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||
self.resize_room(&state, cell);
|
||||
match state {
|
||||
ResizeState::Room(state) => self.resize_room(&state, cell),
|
||||
ResizeState::Text(state) => self.resize_text(&state, cell),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,6 +900,15 @@ impl DungeonApp {
|
||||
false
|
||||
}
|
||||
|
||||
fn start_text_resize(&self, text_idx: usize) -> Option<TextResizeState> {
|
||||
let label = self.layout.text_labels.get(text_idx)?;
|
||||
Some(TextResizeState {
|
||||
text_idx,
|
||||
origin_cell: label.cell,
|
||||
original_font_size: label.font_size,
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve the room that should respond to a resize click, including outer corner targets.
|
||||
fn resize_room_at_pointer(
|
||||
&self,
|
||||
@@ -937,12 +1048,49 @@ impl DungeonApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_text(&mut self, state: &TextResizeState, target_cell: (usize, usize)) {
|
||||
let Some(label) = self.layout.text_labels.get_mut(state.text_idx) else {
|
||||
self.resize_state = None;
|
||||
return;
|
||||
};
|
||||
|
||||
let dx = target_cell.0 as isize - state.origin_cell.0 as isize;
|
||||
let dy = target_cell.1 as isize - state.origin_cell.1 as isize;
|
||||
let delta_cells = if dx.abs() >= dy.abs() { dx } else { -dy };
|
||||
let new_size = (state.original_font_size as isize + (delta_cells * 4)).clamp(8, 96);
|
||||
label.font_size = new_size as u16;
|
||||
}
|
||||
|
||||
// Draw dashed overlays for room resizing interactions.
|
||||
fn draw_resize_overlay(&self, painter: &egui::Painter, geometry: &GridGeometry) {
|
||||
if let Some(text_idx) = self
|
||||
.resize_state
|
||||
.as_ref()
|
||||
.and_then(|s| match s {
|
||||
ResizeState::Text(state) => Some(state.text_idx),
|
||||
ResizeState::Room(_) => None,
|
||||
})
|
||||
.or(self.hover_text_idx)
|
||||
{
|
||||
if let Some(label) = self.layout.text_labels.get(text_idx) {
|
||||
let rect = text_label_rect(geometry, label).expand(3.0);
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
Stroke::new(2.0, Color32::from_gray(180)),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let room_idx = self
|
||||
.resize_state
|
||||
.as_ref()
|
||||
.map(|s| s.room_idx)
|
||||
.and_then(|s| match s {
|
||||
ResizeState::Room(state) => Some(state.room_idx),
|
||||
ResizeState::Text(_) => None,
|
||||
})
|
||||
.or(self.hover_room_idx);
|
||||
let Some(room_idx) = room_idx else {
|
||||
return;
|
||||
@@ -980,7 +1128,7 @@ impl DungeonApp {
|
||||
);
|
||||
painter.text(center_left, egui::Align2::CENTER_CENTER, "^v", font, color);
|
||||
|
||||
if self.resize_state.is_some() {
|
||||
if matches!(self.resize_state, Some(ResizeState::Room(_))) {
|
||||
draw_resize_handle(painter, tl, ResizeCorner::TopLeft, color);
|
||||
draw_resize_handle(painter, tr, ResizeCorner::TopRight, color);
|
||||
draw_resize_handle(painter, br, ResizeCorner::BottomRight, color);
|
||||
@@ -1029,6 +1177,14 @@ impl DungeonApp {
|
||||
|
||||
// Delete the door, room, or corridor at the pointer position.
|
||||
fn delete_at_pointer(&mut self, pointer_pos: egui::Pos2, geometry: &GridGeometry) {
|
||||
if let Some(idx) = self.text_at_pointer(pointer_pos, geometry) {
|
||||
self.push_undo_snapshot();
|
||||
if idx < self.layout.text_labels.len() {
|
||||
self.layout.text_labels.remove(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) {
|
||||
let target = normalized_edge(edge.0, edge.1);
|
||||
if let Some(idx) = self.layout.doors.iter().position(|d| {
|
||||
@@ -1274,6 +1430,11 @@ struct RoomDragState {
|
||||
offset_y: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TextDragState {
|
||||
text_idx: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CorridorDragState {
|
||||
corridor_index: usize,
|
||||
@@ -1304,9 +1465,23 @@ struct RoomResizeState {
|
||||
#[derive(Debug, Clone)]
|
||||
enum DragState {
|
||||
Room(RoomDragState),
|
||||
Text(TextDragState),
|
||||
Corridor(CorridorDragState),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TextResizeState {
|
||||
text_idx: usize,
|
||||
origin_cell: (usize, usize),
|
||||
original_font_size: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ResizeState {
|
||||
Room(RoomResizeState),
|
||||
Text(TextResizeState),
|
||||
}
|
||||
|
||||
struct GridGeometry {
|
||||
rect: egui::Rect,
|
||||
cell_size: f32,
|
||||
@@ -1370,6 +1545,7 @@ fn draw_layout(
|
||||
geometry: &GridGeometry,
|
||||
layout: &DungeonLayout,
|
||||
colorblind_mode: bool,
|
||||
hover_text_idx: Option<usize>,
|
||||
) {
|
||||
let wall_width_px = (geometry.cell_size / 5.0).max(1.0);
|
||||
let door_width_px = (geometry.cell_size / 10.0).max(1.0);
|
||||
@@ -1597,6 +1773,30 @@ fn draw_layout(
|
||||
style,
|
||||
);
|
||||
}
|
||||
|
||||
for (idx, label) in layout.text_labels.iter().enumerate() {
|
||||
if hover_text_idx == Some(idx) {
|
||||
painter.rect_stroke(
|
||||
text_label_rect(geometry, label).expand(2.0),
|
||||
0.0,
|
||||
Stroke::new(2.0, Color32::from_rgb(255, 220, 120)),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
let pos = cell_center(geometry, label.cell.0, label.cell.1);
|
||||
let color = if colorblind_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::WHITE
|
||||
};
|
||||
painter.text(
|
||||
pos,
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&label.text,
|
||||
egui::FontId::proportional(label.font_size as f32),
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the rectangle for a given grid cell.
|
||||
@@ -2224,6 +2424,15 @@ fn cell_center(geometry: &GridGeometry, col: usize, row: usize) -> egui::Pos2 {
|
||||
)
|
||||
}
|
||||
|
||||
fn text_label_rect(geometry: &GridGeometry, label: &layout::TextLabel) -> egui::Rect {
|
||||
let center = cell_center(geometry, label.cell.0, label.cell.1);
|
||||
let font_size = label.font_size as f32;
|
||||
let width =
|
||||
(label.text.chars().count().max(1) as f32 * font_size * 0.6).max(geometry.cell_size * 0.5);
|
||||
let height = font_size.max(geometry.cell_size * 0.35);
|
||||
egui::Rect::from_center_size(center, egui::vec2(width, height))
|
||||
}
|
||||
|
||||
// Draw a crosshatch pattern inside a room for colorblind mode.
|
||||
fn draw_room_crosshatch(
|
||||
painter: &egui::Painter,
|
||||
|
||||
@@ -130,6 +130,7 @@ pub struct UiSettings {
|
||||
pub allow_export_aspect_change: bool,
|
||||
pub export_show_grid: bool,
|
||||
pub add_tool: AddTool,
|
||||
pub add_text_value: String,
|
||||
active_tab: Tab,
|
||||
}
|
||||
|
||||
@@ -168,6 +169,7 @@ impl Default for UiSettings {
|
||||
allow_export_aspect_change: false,
|
||||
export_show_grid: true,
|
||||
add_tool: AddTool::None,
|
||||
add_text_value: "Text".to_string(),
|
||||
active_tab: Tab::Generate,
|
||||
}
|
||||
}
|
||||
@@ -181,6 +183,7 @@ pub enum AddTool {
|
||||
Door,
|
||||
LockedDoor,
|
||||
SecretDoor,
|
||||
Text,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
@@ -713,6 +716,7 @@ fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings) {
|
||||
AddTool::SecretDoor,
|
||||
"Add Secret Door",
|
||||
);
|
||||
ui.selectable_value(&mut settings.add_tool, AddTool::Text, "Add Text");
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
@@ -722,6 +726,15 @@ fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings) {
|
||||
AddTool::Door => ui.label("Click a room/corridor edge to place a door."),
|
||||
AddTool::LockedDoor => ui.label("Click a room/corridor edge to place a locked door."),
|
||||
AddTool::SecretDoor => ui.label("Click a room/corridor edge to place a secret door."),
|
||||
AddTool::Text => {
|
||||
ui.label("Enter the label text, then click a grid cell to place it.");
|
||||
ui.add_space(6.0);
|
||||
ui.label("Text");
|
||||
let response = ui.text_edit_singleline(&mut settings.add_text_value);
|
||||
ui.add_space(4.0);
|
||||
ui.label("Note: Left click on the canvas to place the text.");
|
||||
response
|
||||
}
|
||||
AddTool::None => ui.label("Select a tool to add rooms or corridors."),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user