Add outline popup window for folder context menu
Implement the outline file popup window that opens when a user right-clicks on a folder in the file list (e.g., Act 1 in 06-First Draft) and selects 'Outline' from the context menu. The window displays the corresponding outline file (e.g., 04-Outline/scene_breakdown/act_01.md) in a read-only view. Changes: - Add outline_popup_window() function in file_list.rs - Add window rendering call in mod.rs update loop - Popup reads file content on open and displays it in scrollable view Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
+98
-4
@@ -4,6 +4,39 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Convert a manuscript folder path to its corresponding outline file path.
|
||||
///
|
||||
/// For the snowflake template:
|
||||
/// - `06-First Draft/Act 1` → `04-Outline/scene_breakdown/act_01.md`
|
||||
/// - `06-First Draft/Act 2` → `04-Outline/scene_breakdown/act_02.md`
|
||||
/// - etc.
|
||||
fn outline_path_for_folder(folder_path: &str) -> Option<String> {
|
||||
// Split the path into components
|
||||
let parts: Vec<&str> = folder_path.split('/').collect();
|
||||
|
||||
// We expect paths like "06-First Draft/Act 1"
|
||||
if parts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let last_part = parts.last()?;
|
||||
let folder_name = last_part.trim();
|
||||
|
||||
// Try to extract the act number from the folder name
|
||||
// "Act 1", "Act 2", "Act 3" etc.
|
||||
let act_num = if folder_name.starts_with("Act ") {
|
||||
folder_name.trim_start_matches("Act ").trim().parse::<u32>().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
act_num.map(|n| {
|
||||
// Format as act_01, act_02, etc.
|
||||
let act_str = if n < 10 { format!("act_0{}", n) } else { format!("act_{}", n) };
|
||||
format!("04-Outline/scene_breakdown/{}.md", act_str)
|
||||
})
|
||||
}
|
||||
|
||||
/// Horizontal indent per folder level, in points.
|
||||
const INDENT: f32 = 12.0;
|
||||
|
||||
@@ -175,6 +208,7 @@ impl App {
|
||||
// What the right-click menu picked, applied further down: the
|
||||
// row closure below is still borrowing `self` to draw with.
|
||||
let mut action: Option<FileAction> = None;
|
||||
let mut outline_action: Option<String> = None;
|
||||
let pointer = ui.input(|i| i.pointer.interact_pos());
|
||||
// Filtering happens on the flat list, so folders left with no
|
||||
// files simply stop appearing.
|
||||
@@ -237,18 +271,28 @@ impl App {
|
||||
// widened. The full path is on hover.
|
||||
let name_w =
|
||||
(row_width - indent - COUNT_W).max(48.0);
|
||||
if ui
|
||||
let button_response = ui
|
||||
.add_sized(
|
||||
[name_w, 20.0],
|
||||
egui::Button::new(text)
|
||||
.frame(false)
|
||||
.truncate(),
|
||||
)
|
||||
.on_hover_text(&row.path)
|
||||
.clicked()
|
||||
{
|
||||
.on_hover_text(&row.path);
|
||||
|
||||
if button_response.clicked() {
|
||||
toggled = Some(row.path.clone());
|
||||
}
|
||||
|
||||
// Add context menu for folders that have corresponding outline files
|
||||
if let Some(outline_path) = outline_path_for_folder(&row.path) {
|
||||
button_response.context_menu(|ui| {
|
||||
if ui.button("Outline").clicked() {
|
||||
outline_action = Some(outline_path);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new(count.to_string())
|
||||
.small()
|
||||
@@ -455,6 +499,10 @@ impl App {
|
||||
let ctx = ui.ctx().clone();
|
||||
self.apply_file_action(action, &ctx);
|
||||
}
|
||||
if let Some(outline_path) = outline_action {
|
||||
// Open the outline file
|
||||
self.open_outline_file = Some(outline_path);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
@@ -879,3 +927,49 @@ mod tests {
|
||||
assert!(sketch(&[], &[]).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// The outline file popup window: displays the content of an outline file
|
||||
/// opened from a folder context menu.
|
||||
pub(super) fn outline_popup_window(&mut self, ctx: &egui::Context) {
|
||||
let outline_path = match self.open_outline_file.clone() {
|
||||
Some(p) => p,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let abs_path = self.path_for(&outline_path);
|
||||
let content = match std::fs::read_to_string(&abs_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
self.status = format!("Could not read {}: {}", outline_path, e);
|
||||
self.open_outline_file = None;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut open = true;
|
||||
|
||||
egui::Window::new(format!("Outline — {}", outline_path))
|
||||
.open(&mut open)
|
||||
.resizable(true)
|
||||
.collapsible(false)
|
||||
.default_width(680.0)
|
||||
.default_height(460.0)
|
||||
.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut content.clone())
|
||||
.code_editor()
|
||||
.desired_width(f32::INFINITY)
|
||||
.interactive(false),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if !open {
|
||||
self.open_outline_file = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user