added initial level functionality
This commit is contained in:
+319
-157
@@ -33,7 +33,7 @@ enum ManualDoorKind {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AppSnapshot {
|
||||
settings: UiSettings,
|
||||
layout: DungeonLayout,
|
||||
levels: Vec<DungeonLayout>,
|
||||
suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fn main() -> eframe::Result<()> {
|
||||
|
||||
struct DungeonApp {
|
||||
settings: UiSettings,
|
||||
layout: DungeonLayout,
|
||||
levels: Vec<DungeonLayout>,
|
||||
suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||
undo_stack: Vec<AppSnapshot>,
|
||||
redo_stack: Vec<AppSnapshot>,
|
||||
@@ -70,26 +70,10 @@ impl Default for DungeonApp {
|
||||
// Build the app with persisted settings and a generated layout.
|
||||
fn default() -> Self {
|
||||
let settings = settings::load_settings().unwrap_or_default();
|
||||
let layout = layout::generate_layout(
|
||||
settings.cols,
|
||||
settings.rows,
|
||||
settings.room_count,
|
||||
settings.seed,
|
||||
settings.min_room_size,
|
||||
settings.max_room_size,
|
||||
settings.square_rooms_only,
|
||||
settings.min_corridor_width,
|
||||
settings.max_corridor_width,
|
||||
settings.corridor_randomness,
|
||||
settings.dead_end_rooms_percent,
|
||||
settings.pack_rooms_without_corridors,
|
||||
door_settings_from_ui(&settings),
|
||||
window_settings_from_ui(&settings),
|
||||
);
|
||||
let layout = populate_random_markers(layout, &settings);
|
||||
let levels = generate_all_levels(&settings);
|
||||
Self {
|
||||
settings,
|
||||
layout,
|
||||
levels,
|
||||
suppressed_auto_door_edges: HashSet::new(),
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
@@ -146,16 +130,20 @@ impl eframe::App for DungeonApp {
|
||||
self.push_undo_snapshot();
|
||||
self.regenerate_layout();
|
||||
} else if panel_result.save_clicked {
|
||||
let svg = exporter::build_svg(&self.layout, &self.settings);
|
||||
if let Err(err) = saveandload::save_dungeon(&self.settings, &self.layout, svg) {
|
||||
if self.settings.active_level_index < self.levels.len() {
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
let svg = exporter::build_svg(layout, &self.settings);
|
||||
if let Err(err) = saveandload::save_dungeon(&self.settings, layout, svg) {
|
||||
eprintln!("{err}");
|
||||
}
|
||||
}
|
||||
} else if panel_result.load_clicked {
|
||||
match saveandload::load_dungeon() {
|
||||
Ok(data) => {
|
||||
self.push_undo_snapshot();
|
||||
self.settings = data.settings;
|
||||
self.layout = data.layout;
|
||||
self.levels = vec![data.layout];
|
||||
self.settings.active_level_index = 0;
|
||||
// Don't regenerate_layout() here to preserve the exact loaded state.
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -187,7 +175,8 @@ impl eframe::App for DungeonApp {
|
||||
}
|
||||
}
|
||||
|
||||
let layout = self.layout.clone();
|
||||
if self.settings.active_level_index < self.levels.len() {
|
||||
let layout = self.levels[self.settings.active_level_index].clone();
|
||||
let settings = self.settings.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
thread::spawn(move || {
|
||||
@@ -196,6 +185,7 @@ impl eframe::App for DungeonApp {
|
||||
});
|
||||
self.export_rx = Some(rx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rx) = &self.export_rx {
|
||||
match rx.try_recv() {
|
||||
@@ -270,6 +260,19 @@ impl eframe::App for DungeonApp {
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
for i in 0..self.levels.len() {
|
||||
let label = format!("Level {}", i + 1);
|
||||
if ui
|
||||
.selectable_label(self.settings.active_level_index == i, label)
|
||||
.clicked()
|
||||
{
|
||||
self.settings.active_level_index = i;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
let available = ui.available_size_before_wrap();
|
||||
let (response, painter) = ui.allocate_painter(available, egui::Sense::click_and_drag());
|
||||
let canvas = response.rect.shrink(12.0);
|
||||
@@ -286,14 +289,17 @@ impl eframe::App for DungeonApp {
|
||||
}
|
||||
self.pending_delete = false;
|
||||
}
|
||||
|
||||
if self.settings.active_level_index < self.levels.len() {
|
||||
draw_layout(
|
||||
&painter,
|
||||
&geometry,
|
||||
&self.layout,
|
||||
&self.levels[self.settings.active_level_index],
|
||||
self.settings.colorblind_mode,
|
||||
self.hover_text_idx,
|
||||
self.hover_marker,
|
||||
);
|
||||
}
|
||||
self.draw_add_overlay(&painter, &geometry);
|
||||
self.draw_resize_overlay(&painter, &geometry);
|
||||
self.draw_corridor_hover_overlay(&painter, &geometry);
|
||||
@@ -302,6 +308,40 @@ impl eframe::App for DungeonApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_all_levels(settings: &UiSettings) -> Vec<DungeonLayout> {
|
||||
let level_count = if settings.min_levels == settings.max_levels {
|
||||
settings.min_levels
|
||||
} else {
|
||||
let range_seed = seed::derive_seed(settings.seed, 0x1E_7E_1_u64);
|
||||
(range_seed as usize % (settings.max_levels - settings.min_levels + 1))
|
||||
+ settings.min_levels
|
||||
};
|
||||
|
||||
let mut levels = Vec::with_capacity(level_count);
|
||||
for i in 0..level_count {
|
||||
let level_seed = seed::derive_seed(settings.seed, i as u64);
|
||||
let layout = layout::generate_layout(
|
||||
settings.cols,
|
||||
settings.rows,
|
||||
settings.room_count,
|
||||
level_seed,
|
||||
settings.min_room_size,
|
||||
settings.max_room_size,
|
||||
settings.square_rooms_only,
|
||||
settings.min_corridor_width,
|
||||
settings.max_corridor_width,
|
||||
settings.corridor_randomness,
|
||||
settings.dead_end_rooms_percent,
|
||||
settings.pack_rooms_without_corridors,
|
||||
door_settings_from_ui(settings),
|
||||
window_settings_from_ui(settings),
|
||||
);
|
||||
let layout = populate_random_markers(layout, settings);
|
||||
levels.push(layout);
|
||||
}
|
||||
levels
|
||||
}
|
||||
|
||||
fn clamp_dependent_settings(settings: &mut UiSettings) {
|
||||
if settings.min_room_size > settings.max_room_size {
|
||||
settings.max_room_size = settings.min_room_size;
|
||||
@@ -333,6 +373,12 @@ fn clamp_dependent_settings(settings: &mut UiSettings) {
|
||||
if settings.min_monsters_per_area > settings.max_monsters_per_area {
|
||||
settings.max_monsters_per_area = settings.min_monsters_per_area;
|
||||
}
|
||||
if settings.min_levels == 0 {
|
||||
settings.min_levels = 1;
|
||||
}
|
||||
if settings.min_levels > settings.max_levels {
|
||||
settings.max_levels = settings.min_levels;
|
||||
}
|
||||
}
|
||||
|
||||
impl DungeonApp {
|
||||
@@ -355,7 +401,7 @@ impl DungeonApp {
|
||||
fn snapshot(&self) -> AppSnapshot {
|
||||
AppSnapshot {
|
||||
settings: self.settings.clone(),
|
||||
layout: self.layout.clone(),
|
||||
levels: self.levels.clone(),
|
||||
suppressed_auto_door_edges: self.suppressed_auto_door_edges.clone(),
|
||||
}
|
||||
}
|
||||
@@ -374,7 +420,7 @@ impl DungeonApp {
|
||||
|
||||
fn restore_snapshot(&mut self, snapshot: AppSnapshot) {
|
||||
self.settings = snapshot.settings;
|
||||
self.layout = snapshot.layout;
|
||||
self.levels = snapshot.levels;
|
||||
self.suppressed_auto_door_edges = snapshot.suppressed_auto_door_edges;
|
||||
self.reset_transient_state();
|
||||
}
|
||||
@@ -397,7 +443,10 @@ impl DungeonApp {
|
||||
|
||||
// Clear the current layout and reset transient interaction state.
|
||||
fn clear_layout(&mut self) {
|
||||
self.layout = DungeonLayout {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
self.levels[self.settings.active_level_index] = DungeonLayout {
|
||||
packed_rooms: self.settings.pack_rooms_without_corridors,
|
||||
..DungeonLayout::default()
|
||||
};
|
||||
@@ -409,23 +458,10 @@ impl DungeonApp {
|
||||
fn regenerate_layout(&mut self) {
|
||||
self.drag_state = None;
|
||||
self.suppressed_auto_door_edges.clear();
|
||||
let layout = layout::generate_layout(
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
self.settings.room_count,
|
||||
self.settings.seed,
|
||||
self.settings.min_room_size,
|
||||
self.settings.max_room_size,
|
||||
self.settings.square_rooms_only,
|
||||
self.settings.min_corridor_width,
|
||||
self.settings.max_corridor_width,
|
||||
self.settings.corridor_randomness,
|
||||
self.settings.dead_end_rooms_percent,
|
||||
self.settings.pack_rooms_without_corridors,
|
||||
door_settings_from_ui(&self.settings),
|
||||
window_settings_from_ui(&self.settings),
|
||||
);
|
||||
self.layout = populate_random_markers(layout, &self.settings);
|
||||
self.levels = generate_all_levels(&self.settings);
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
self.settings.active_level_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle dragging rooms or corridors with the primary mouse button.
|
||||
@@ -499,7 +535,12 @@ impl DungeonApp {
|
||||
) -> Option<(usize, f32, f32)> {
|
||||
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||
|
||||
for (room_idx, room) in self.layout.rooms.iter().enumerate().rev() {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
for (room_idx, room) in layout.rooms.iter().enumerate().rev() {
|
||||
let room_x = room.x as f32;
|
||||
let room_y = room.y as f32;
|
||||
let room_right = room_x + room.width as f32;
|
||||
@@ -524,13 +565,17 @@ impl DungeonApp {
|
||||
return;
|
||||
};
|
||||
|
||||
if drag.room_idx >= self.layout.rooms.len() {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
if drag.room_idx >= self.levels[self.settings.active_level_index].rooms.len() {
|
||||
self.drag_state = None;
|
||||
return;
|
||||
}
|
||||
|
||||
let (width, height, old_x, old_y) = {
|
||||
let room = &self.layout.rooms[drag.room_idx];
|
||||
let room = &self.levels[self.settings.active_level_index].rooms[drag.room_idx];
|
||||
(room.width, room.height, room.x, room.y)
|
||||
};
|
||||
let max_x = self.settings.cols.saturating_sub(width);
|
||||
@@ -545,8 +590,8 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
|
||||
self.layout.rooms[drag.room_idx].x = new_x;
|
||||
self.layout.rooms[drag.room_idx].y = new_y;
|
||||
self.levels[self.settings.active_level_index].rooms[drag.room_idx].x = new_x;
|
||||
self.levels[self.settings.active_level_index].rooms[drag.room_idx].y = new_y;
|
||||
self.reroute_corridors_for_room(drag.room_idx);
|
||||
self.refresh_doors();
|
||||
}
|
||||
@@ -561,7 +606,15 @@ impl DungeonApp {
|
||||
return;
|
||||
};
|
||||
|
||||
if drag.text_idx >= self.layout.text_labels.len() {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
if drag.text_idx
|
||||
>= self.levels[self.settings.active_level_index]
|
||||
.text_labels
|
||||
.len()
|
||||
{
|
||||
self.drag_state = None;
|
||||
return;
|
||||
}
|
||||
@@ -571,24 +624,29 @@ impl DungeonApp {
|
||||
(grid_y.floor().max(0.0) as usize).min(self.settings.rows.saturating_sub(1)),
|
||||
);
|
||||
|
||||
self.layout.text_labels[drag.text_idx].cell = target;
|
||||
self.levels[self.settings.active_level_index].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 {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
if layout.packed_rooms {
|
||||
return;
|
||||
}
|
||||
|
||||
for corridor in &mut self.layout.corridors {
|
||||
for corridor in &mut layout.corridors {
|
||||
if corridor.start_room_id != room_idx && corridor.end_room_id != room_idx {
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = self.layout.rooms[corridor.start_room_id].center_cell();
|
||||
let end = self.layout.rooms[corridor.end_room_id].center_cell();
|
||||
let start = layout.rooms[corridor.start_room_id].center_cell();
|
||||
let end = layout.rooms[corridor.end_room_id].center_cell();
|
||||
let blocked = blocked_room_cells(
|
||||
&self.layout.rooms,
|
||||
&layout.rooms,
|
||||
&[corridor.start_room_id, corridor.end_room_id],
|
||||
);
|
||||
if let Some(path) =
|
||||
@@ -607,15 +665,19 @@ impl DungeonApp {
|
||||
) -> Option<CorridorDragState> {
|
||||
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||
let corridor_index = self
|
||||
.layout
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
let corridor_index = layout
|
||||
.corridors
|
||||
.iter()
|
||||
.position(|corridor| corridor.path.contains(&clicked))?;
|
||||
|
||||
Some(CorridorDragState {
|
||||
corridor_index,
|
||||
original_path: self.layout.corridors[corridor_index].path.clone(),
|
||||
original_path: layout.corridors[corridor_index].path.clone(),
|
||||
original_cell: clicked,
|
||||
})
|
||||
}
|
||||
@@ -631,11 +693,16 @@ impl DungeonApp {
|
||||
return;
|
||||
};
|
||||
|
||||
if drag.corridor_index >= self.layout.corridors.len() {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
if drag.corridor_index >= layout.corridors.len() {
|
||||
self.drag_state = None;
|
||||
return;
|
||||
}
|
||||
let corridor = &self.layout.corridors[drag.corridor_index];
|
||||
let corridor = &layout.corridors[drag.corridor_index];
|
||||
|
||||
let target = (
|
||||
(grid_x.floor().max(0.0) as usize).min(self.settings.cols.saturating_sub(1)),
|
||||
@@ -649,33 +716,38 @@ impl DungeonApp {
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
&blocked_room_cells(
|
||||
&self.layout.rooms,
|
||||
&layout.rooms,
|
||||
&[corridor.start_room_id, corridor.end_room_id],
|
||||
),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.layout.corridors[drag.corridor_index].path = new_path;
|
||||
layout.corridors[drag.corridor_index].path = new_path;
|
||||
self.refresh_doors();
|
||||
}
|
||||
|
||||
// Rebuild doors based on the current layout and settings.
|
||||
fn refresh_doors(&mut self) {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
layout::apply_doors(
|
||||
&mut self.layout,
|
||||
layout,
|
||||
self.settings.seed,
|
||||
door_settings_from_ui(&self.settings),
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
);
|
||||
self.layout.doors.retain(|door| {
|
||||
layout.doors.retain(|door| {
|
||||
!door_edges_for(door, self.settings.cols, self.settings.rows)
|
||||
.iter()
|
||||
.any(|edge| self.suppressed_auto_door_edges.contains(edge))
|
||||
});
|
||||
layout::apply_windows(
|
||||
&mut self.layout,
|
||||
layout,
|
||||
self.settings.seed,
|
||||
window_settings_from_ui(&self.settings),
|
||||
self.settings.cols,
|
||||
@@ -794,6 +866,11 @@ impl DungeonApp {
|
||||
// Add a room anchored to the given grid cell if space allows.
|
||||
fn add_room_at_cell(&mut self, cell: (usize, usize)) {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
let width = self.settings.min_room_size.max(1);
|
||||
let height = self.settings.min_room_size.max(1);
|
||||
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
|
||||
@@ -814,8 +891,7 @@ impl DungeonApp {
|
||||
height,
|
||||
};
|
||||
|
||||
if self
|
||||
.layout
|
||||
if layout
|
||||
.rooms
|
||||
.iter()
|
||||
.any(|room| rooms_overlap(&new_room, room))
|
||||
@@ -823,24 +899,29 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
|
||||
self.layout.rooms.push(new_room);
|
||||
layout.rooms.push(new_room);
|
||||
self.refresh_doors();
|
||||
}
|
||||
|
||||
// Add a corridor between two room cells using a shortest path.
|
||||
fn add_corridor_between(&mut self, start: (usize, usize), end: (usize, usize)) {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
if start == end {
|
||||
return;
|
||||
}
|
||||
|
||||
let start_room = room_index_at_cell(&self.layout.rooms, start);
|
||||
let end_room = room_index_at_cell(&self.layout.rooms, end);
|
||||
let start_room = room_index_at_cell(&layout.rooms, start);
|
||||
let end_room = room_index_at_cell(&layout.rooms, end);
|
||||
let (Some(start_room), Some(end_room)) = (start_room, end_room) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let blocked = blocked_room_cells(&self.layout.rooms, &[start_room, end_room]);
|
||||
let blocked = blocked_room_cells(&layout.rooms, &[start_room, end_room]);
|
||||
|
||||
let Some(path) =
|
||||
shortest_path_cells(start, end, self.settings.cols, self.settings.rows, &blocked)
|
||||
@@ -848,8 +929,7 @@ impl DungeonApp {
|
||||
return;
|
||||
};
|
||||
|
||||
let next_id = self
|
||||
.layout
|
||||
let next_id = layout
|
||||
.corridors
|
||||
.iter()
|
||||
.map(|c| c.id)
|
||||
@@ -858,7 +938,7 @@ impl DungeonApp {
|
||||
.wrapping_add(1);
|
||||
let width = self.settings.min_corridor_width.max(1);
|
||||
|
||||
self.layout.corridors.push(layout::Corridor {
|
||||
layout.corridors.push(layout::Corridor {
|
||||
id: next_id,
|
||||
start_room_id: start_room,
|
||||
end_room_id: end_room,
|
||||
@@ -869,6 +949,12 @@ impl DungeonApp {
|
||||
}
|
||||
|
||||
fn add_text_at_cell(&mut self, cell: (usize, usize)) {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
|
||||
return;
|
||||
}
|
||||
@@ -878,9 +964,7 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
|
||||
self.push_undo_snapshot();
|
||||
if let Some(existing) = self
|
||||
.layout
|
||||
if let Some(existing) = layout
|
||||
.text_labels
|
||||
.iter_mut()
|
||||
.find(|label| label.cell == cell)
|
||||
@@ -889,7 +973,7 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
|
||||
self.layout.text_labels.push(layout::TextLabel {
|
||||
layout.text_labels.push(layout::TextLabel {
|
||||
cell,
|
||||
text,
|
||||
font_size: 18,
|
||||
@@ -897,6 +981,12 @@ impl DungeonApp {
|
||||
}
|
||||
|
||||
fn add_marker_at_cell(&mut self, cell: (usize, usize), kind: MarkerKind) {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
|
||||
return;
|
||||
}
|
||||
@@ -910,12 +1000,11 @@ impl DungeonApp {
|
||||
let origin = centered_marker_origin(cell, size, self.settings.cols, self.settings.rows);
|
||||
let marker = layout::AreaMarker { cell: origin, size };
|
||||
|
||||
self.push_undo_snapshot();
|
||||
match kind {
|
||||
MarkerKind::Start => self.layout.start_markers.push(marker),
|
||||
MarkerKind::End => self.layout.end_markers.push(marker),
|
||||
MarkerKind::Trap => self.layout.trap_markers.push(marker),
|
||||
MarkerKind::Monster => self.layout.monster_markers.push(marker),
|
||||
MarkerKind::Start => layout.start_markers.push(marker),
|
||||
MarkerKind::End => layout.end_markers.push(marker),
|
||||
MarkerKind::Trap => layout.trap_markers.push(marker),
|
||||
MarkerKind::Monster => layout.monster_markers.push(marker),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -925,9 +1014,14 @@ impl DungeonApp {
|
||||
self.clear_hover_targets();
|
||||
return;
|
||||
};
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) {
|
||||
let target = normalized_edge(edge.0, edge.1);
|
||||
self.hover_door_idx = self.layout.doors.iter().position(|d| {
|
||||
self.hover_door_idx = layout.doors.iter().position(|d| {
|
||||
door_edges_for(d, self.settings.cols, self.settings.rows)
|
||||
.iter()
|
||||
.any(|e| normalized_edge(e.0, e.1) == target)
|
||||
@@ -980,7 +1074,12 @@ 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
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
layout
|
||||
.text_labels
|
||||
.iter()
|
||||
.position(|label| label.cell == clicked)
|
||||
@@ -993,14 +1092,19 @@ impl DungeonApp {
|
||||
) -> Option<HoverMarker> {
|
||||
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
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
layout
|
||||
.start_markers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, marker)| marker_contains_cell(Some(marker), clicked))
|
||||
.map(|(idx, _)| HoverMarker::Start(idx))
|
||||
.or_else(|| {
|
||||
self.layout
|
||||
layout
|
||||
.end_markers
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1008,7 +1112,7 @@ impl DungeonApp {
|
||||
.map(|(idx, _)| HoverMarker::End(idx))
|
||||
})
|
||||
.or_else(|| {
|
||||
self.layout
|
||||
layout
|
||||
.trap_markers
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1016,7 +1120,7 @@ impl DungeonApp {
|
||||
.map(|(idx, _)| HoverMarker::Trap(idx))
|
||||
})
|
||||
.or_else(|| {
|
||||
self.layout
|
||||
layout
|
||||
.monster_markers
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1031,9 +1135,14 @@ impl DungeonApp {
|
||||
geometry: &GridGeometry,
|
||||
) -> Option<MarkerDragState> {
|
||||
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
match self.marker_at_pointer(pointer_pos, geometry)? {
|
||||
HoverMarker::Start(marker_idx) => {
|
||||
let marker = self.layout.start_markers.get(marker_idx)?;
|
||||
let marker = layout.start_markers.get(marker_idx)?;
|
||||
Some(MarkerDragState {
|
||||
kind: MarkerKind::Start,
|
||||
marker_idx,
|
||||
@@ -1042,7 +1151,7 @@ impl DungeonApp {
|
||||
})
|
||||
}
|
||||
HoverMarker::End(marker_idx) => {
|
||||
let marker = self.layout.end_markers.get(marker_idx)?;
|
||||
let marker = layout.end_markers.get(marker_idx)?;
|
||||
Some(MarkerDragState {
|
||||
kind: MarkerKind::End,
|
||||
marker_idx,
|
||||
@@ -1051,7 +1160,7 @@ impl DungeonApp {
|
||||
})
|
||||
}
|
||||
HoverMarker::Trap(marker_idx) => {
|
||||
let marker = self.layout.trap_markers.get(marker_idx)?;
|
||||
let marker = layout.trap_markers.get(marker_idx)?;
|
||||
Some(MarkerDragState {
|
||||
kind: MarkerKind::Trap,
|
||||
marker_idx,
|
||||
@@ -1060,7 +1169,7 @@ impl DungeonApp {
|
||||
})
|
||||
}
|
||||
HoverMarker::Monster(marker_idx) => {
|
||||
let marker = self.layout.monster_markers.get(marker_idx)?;
|
||||
let marker = layout.monster_markers.get(marker_idx)?;
|
||||
Some(MarkerDragState {
|
||||
kind: MarkerKind::Monster,
|
||||
marker_idx,
|
||||
@@ -1081,11 +1190,16 @@ impl DungeonApp {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
let markers = match drag.kind {
|
||||
MarkerKind::Start => &mut self.layout.start_markers,
|
||||
MarkerKind::End => &mut self.layout.end_markers,
|
||||
MarkerKind::Trap => &mut self.layout.trap_markers,
|
||||
MarkerKind::Monster => &mut self.layout.monster_markers,
|
||||
MarkerKind::Start => &mut layout.start_markers,
|
||||
MarkerKind::End => &mut layout.end_markers,
|
||||
MarkerKind::Trap => &mut layout.trap_markers,
|
||||
MarkerKind::Monster => &mut layout.monster_markers,
|
||||
};
|
||||
let Some(marker) = markers.get_mut(drag.marker_idx) else {
|
||||
self.drag_state = None;
|
||||
@@ -1152,7 +1266,12 @@ impl DungeonApp {
|
||||
}
|
||||
|
||||
fn start_text_resize(&self, text_idx: usize) -> Option<TextResizeState> {
|
||||
let label = self.layout.text_labels.get(text_idx)?;
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
let label = layout.text_labels.get(text_idx)?;
|
||||
Some(TextResizeState {
|
||||
text_idx,
|
||||
origin_cell: label.cell,
|
||||
@@ -1176,14 +1295,13 @@ impl DungeonApp {
|
||||
grid_y.floor().max(0.0) as usize,
|
||||
);
|
||||
|
||||
for (room_idx, room) in self.layout.rooms.iter().enumerate().rev() {
|
||||
if resize_hit_cells(
|
||||
room,
|
||||
room_idx,
|
||||
&self.layout.rooms,
|
||||
geometry.cols,
|
||||
geometry.rows,
|
||||
)
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
for (room_idx, room) in layout.rooms.iter().enumerate().rev() {
|
||||
if resize_hit_cells(room, room_idx, &layout.rooms, geometry.cols, geometry.rows)
|
||||
.contains(&clicked)
|
||||
{
|
||||
return Some(room_idx);
|
||||
@@ -1200,7 +1318,12 @@ impl DungeonApp {
|
||||
pointer_pos: egui::Pos2,
|
||||
geometry: &GridGeometry,
|
||||
) -> Option<RoomResizeState> {
|
||||
let original_room = self.layout.rooms.get(room_idx)?.clone();
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
let original_room = layout.rooms.get(room_idx)?.clone();
|
||||
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||
let center_x = original_room.x as f32 + original_room.width as f32 * 0.5;
|
||||
let center_y = original_room.y as f32 + original_room.height as f32 * 0.5;
|
||||
@@ -1223,6 +1346,11 @@ impl DungeonApp {
|
||||
|
||||
// Resize only the corner chosen at drag start while keeping the opposite corner fixed.
|
||||
fn resize_room(&mut self, state: &RoomResizeState, target_cell: (usize, usize)) {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
let cols = self.settings.cols.max(1);
|
||||
let rows = self.settings.rows.max(1);
|
||||
|
||||
@@ -1256,8 +1384,7 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
|
||||
if self
|
||||
.layout
|
||||
if layout
|
||||
.rooms
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1266,7 +1393,7 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(room) = self.layout.rooms.get_mut(state.room_idx) {
|
||||
if let Some(room) = layout.rooms.get_mut(state.room_idx) {
|
||||
room.x = new_room.x;
|
||||
room.y = new_room.y;
|
||||
room.width = new_room.width;
|
||||
@@ -1277,7 +1404,12 @@ 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 {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
let Some(label) = layout.text_labels.get_mut(state.text_idx) else {
|
||||
self.resize_state = None;
|
||||
return;
|
||||
};
|
||||
@@ -1291,6 +1423,11 @@ impl DungeonApp {
|
||||
|
||||
// Draw dashed overlays for room resizing interactions.
|
||||
fn draw_resize_overlay(&self, painter: &egui::Painter, geometry: &GridGeometry) {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
if let Some(text_idx) = self
|
||||
.resize_state
|
||||
.as_ref()
|
||||
@@ -1300,7 +1437,7 @@ impl DungeonApp {
|
||||
})
|
||||
.or(self.hover_text_idx)
|
||||
{
|
||||
if let Some(label) = self.layout.text_labels.get(text_idx) {
|
||||
if let Some(label) = layout.text_labels.get(text_idx) {
|
||||
let rect = text_label_rect(geometry, label).expand(3.0);
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
@@ -1323,7 +1460,7 @@ impl DungeonApp {
|
||||
let Some(room_idx) = room_idx else {
|
||||
return;
|
||||
};
|
||||
let Some(room) = self.layout.rooms.get(room_idx) else {
|
||||
let Some(room) = layout.rooms.get(room_idx) else {
|
||||
return;
|
||||
};
|
||||
let color = Color32::from_gray(170);
|
||||
@@ -1369,7 +1506,12 @@ impl DungeonApp {
|
||||
let Some(idx) = self.hover_corridor_idx else {
|
||||
return;
|
||||
};
|
||||
let Some(corridor) = self.layout.corridors.get(idx) else {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
let Some(corridor) = layout.corridors.get(idx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1387,7 +1529,12 @@ impl DungeonApp {
|
||||
let Some(idx) = self.hover_door_idx else {
|
||||
return;
|
||||
};
|
||||
let Some(door) = self.layout.doors.get(idx) else {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &self.levels[self.settings.active_level_index];
|
||||
|
||||
let Some(door) = layout.doors.get(idx) else {
|
||||
return;
|
||||
};
|
||||
let color = Color32::from_gray(200);
|
||||
@@ -1405,35 +1552,41 @@ 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 self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
if idx < layout.text_labels.len() {
|
||||
layout.text_labels.remove(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(marker) = self.marker_at_pointer(pointer_pos, geometry) {
|
||||
self.push_undo_snapshot();
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
match marker {
|
||||
HoverMarker::Start(idx) => {
|
||||
if idx < self.layout.start_markers.len() {
|
||||
self.layout.start_markers.remove(idx);
|
||||
if idx < layout.start_markers.len() {
|
||||
layout.start_markers.remove(idx);
|
||||
}
|
||||
}
|
||||
HoverMarker::End(idx) => {
|
||||
if idx < self.layout.end_markers.len() {
|
||||
self.layout.end_markers.remove(idx);
|
||||
if idx < layout.end_markers.len() {
|
||||
layout.end_markers.remove(idx);
|
||||
}
|
||||
}
|
||||
HoverMarker::Trap(idx) => {
|
||||
if idx < self.layout.trap_markers.len() {
|
||||
self.layout.trap_markers.remove(idx);
|
||||
if idx < layout.trap_markers.len() {
|
||||
layout.trap_markers.remove(idx);
|
||||
}
|
||||
}
|
||||
HoverMarker::Monster(idx) => {
|
||||
if idx < self.layout.monster_markers.len() {
|
||||
self.layout.monster_markers.remove(idx);
|
||||
if idx < layout.monster_markers.len() {
|
||||
layout.monster_markers.remove(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1442,25 +1595,25 @@ impl DungeonApp {
|
||||
|
||||
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| {
|
||||
let active_index = self.settings.active_level_index;
|
||||
if let Some(idx) = self.levels[active_index].doors.iter().position(|d| {
|
||||
door_edges_for(d, self.settings.cols, self.settings.rows)
|
||||
.iter()
|
||||
.any(|e| normalized_edge(e.0, e.1) == target)
|
||||
}) {
|
||||
self.push_undo_snapshot();
|
||||
if !self.layout.doors[idx].manual {
|
||||
for edge in door_edges_for(
|
||||
&self.layout.doors[idx],
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
) {
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
if !layout.doors[idx].manual {
|
||||
for edge in
|
||||
door_edges_for(&layout.doors[idx], self.settings.cols, self.settings.rows)
|
||||
{
|
||||
self.suppressed_auto_door_edges.insert(edge);
|
||||
}
|
||||
self.layout.doors.remove(idx);
|
||||
layout.doors.remove(idx);
|
||||
return;
|
||||
}
|
||||
|
||||
self.layout.doors.remove(idx);
|
||||
layout.doors.remove(idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1472,9 +1625,11 @@ impl DungeonApp {
|
||||
}
|
||||
|
||||
if let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry) {
|
||||
if corridor_drag.corridor_index < self.layout.corridors.len() {
|
||||
let active_index = self.settings.active_level_index;
|
||||
if corridor_drag.corridor_index < self.levels[active_index].corridors.len() {
|
||||
self.push_undo_snapshot();
|
||||
self.layout.corridors.remove(corridor_drag.corridor_index);
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
layout.corridors.remove(corridor_drag.corridor_index);
|
||||
self.drag_state = None;
|
||||
self.refresh_doors();
|
||||
}
|
||||
@@ -1483,14 +1638,19 @@ impl DungeonApp {
|
||||
|
||||
// Remove a room and update corridors/doors to match.
|
||||
fn delete_room(&mut self, room_idx: usize) {
|
||||
if room_idx >= self.layout.rooms.len() {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
self.layout.rooms.remove(room_idx);
|
||||
self.layout
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
if room_idx >= layout.rooms.len() {
|
||||
return;
|
||||
}
|
||||
layout.rooms.remove(room_idx);
|
||||
layout
|
||||
.corridors
|
||||
.retain(|c| c.start_room_id != room_idx && c.end_room_id != room_idx);
|
||||
for corridor in &mut self.layout.corridors {
|
||||
for corridor in &mut layout.corridors {
|
||||
if corridor.start_room_id > room_idx {
|
||||
corridor.start_room_id -= 1;
|
||||
}
|
||||
@@ -1509,28 +1669,30 @@ impl DungeonApp {
|
||||
geometry: &GridGeometry,
|
||||
kind: ManualDoorKind,
|
||||
) {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.push_undo_snapshot();
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
let target = normalized_edge(edge.0, edge.1);
|
||||
if let Some(idx) = self.layout.doors.iter().position(|d| {
|
||||
if let Some(idx) = layout.doors.iter().position(|d| {
|
||||
door_edges_for(d, self.settings.cols, self.settings.rows)
|
||||
.iter()
|
||||
.any(|e| normalized_edge(e.0, e.1) == target)
|
||||
}) {
|
||||
let edges = door_edges_for(
|
||||
&self.layout.doors[idx],
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
);
|
||||
let edges = door_edges_for(&layout.doors[idx], self.settings.cols, self.settings.rows);
|
||||
let mut spawned = Vec::new();
|
||||
for edge in edges {
|
||||
if normalized_edge(edge.0, edge.1) == target {
|
||||
continue;
|
||||
}
|
||||
if self
|
||||
.layout
|
||||
if layout
|
||||
.doors
|
||||
.iter()
|
||||
.any(|d| normalized_edge(d.from, d.to) == normalized_edge(edge.0, edge.1))
|
||||
@@ -1548,7 +1710,7 @@ impl DungeonApp {
|
||||
manual: true,
|
||||
});
|
||||
}
|
||||
let door = &mut self.layout.doors[idx];
|
||||
let door = &mut layout.doors[idx];
|
||||
door.locked = kind == ManualDoorKind::Locked;
|
||||
door.archway = kind == ManualDoorKind::Archway;
|
||||
door.secret = kind == ManualDoorKind::Secret;
|
||||
@@ -1558,13 +1720,13 @@ impl DungeonApp {
|
||||
for edge in door_edges_for(door, self.settings.cols, self.settings.rows) {
|
||||
self.suppressed_auto_door_edges.remove(&edge);
|
||||
}
|
||||
self.layout.doors.extend(spawned);
|
||||
layout.doors.extend(spawned);
|
||||
return;
|
||||
}
|
||||
|
||||
let corridor_cells = corridor_cells(&self.layout, self.settings.cols, self.settings.rows);
|
||||
let a_room = room_index_at_cell(&self.layout.rooms, edge.0);
|
||||
let b_room = room_index_at_cell(&self.layout.rooms, edge.1);
|
||||
let corridor_cells = corridor_cells(layout, self.settings.cols, self.settings.rows);
|
||||
let a_room = room_index_at_cell(&layout.rooms, edge.0);
|
||||
let b_room = room_index_at_cell(&layout.rooms, edge.1);
|
||||
let a_in_corridor = corridor_cells.contains(&edge.0);
|
||||
let b_in_corridor = corridor_cells.contains(&edge.1);
|
||||
|
||||
@@ -1572,7 +1734,7 @@ impl DungeonApp {
|
||||
return;
|
||||
}
|
||||
|
||||
self.layout.doors.push(layout::Door {
|
||||
layout.doors.push(layout::Door {
|
||||
from: edge.0,
|
||||
to: edge.1,
|
||||
width: 1,
|
||||
|
||||
@@ -142,6 +142,9 @@ pub struct UiSettings {
|
||||
pub max_start_marker_count: usize,
|
||||
pub min_end_marker_count: usize,
|
||||
pub max_end_marker_count: usize,
|
||||
pub min_levels: usize,
|
||||
pub max_levels: usize,
|
||||
pub active_level_index: usize,
|
||||
pub trap_frequency_percent: usize,
|
||||
pub min_traps_per_area: usize,
|
||||
pub max_traps_per_area: usize,
|
||||
@@ -196,6 +199,9 @@ impl Default for UiSettings {
|
||||
max_start_marker_count: 1,
|
||||
min_end_marker_count: 1,
|
||||
max_end_marker_count: 1,
|
||||
min_levels: 1,
|
||||
max_levels: 1,
|
||||
active_level_index: 0,
|
||||
trap_frequency_percent: 20,
|
||||
min_traps_per_area: 1,
|
||||
max_traps_per_area: 3,
|
||||
@@ -484,6 +490,44 @@ fn draw_generate_tab(
|
||||
|
||||
// Render the Layout tab controls.
|
||||
fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new("Levels").strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label("Min Levels");
|
||||
ui.horizontal(|ui| {
|
||||
result.settings_changed |= ui
|
||||
.add(egui::Slider::new(&mut settings.min_levels, 1..=20).show_value(false))
|
||||
.changed();
|
||||
result.settings_changed |= ui
|
||||
.add(
|
||||
egui::DragValue::new(&mut settings.min_levels)
|
||||
.speed(1.0)
|
||||
.range(1..=100),
|
||||
)
|
||||
.changed();
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label("Max Levels");
|
||||
ui.horizontal(|ui| {
|
||||
result.settings_changed |= ui
|
||||
.add(egui::Slider::new(&mut settings.max_levels, 1..=20).show_value(false))
|
||||
.changed();
|
||||
result.settings_changed |= ui
|
||||
.add(
|
||||
egui::DragValue::new(&mut settings.max_levels)
|
||||
.speed(1.0)
|
||||
.range(1..=100),
|
||||
)
|
||||
.changed();
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new("Room Settings").strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user