From cea944779e01cfe3fdb391683c99b86a91c4d5bf Mon Sep 17 00:00:00 2001 From: landon Date: Sun, 13 Sep 2026 10:47:11 -0500 Subject: [PATCH] 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 --- src/app/file_list.rs | 102 +++++++++++++++++++++++++++++++++++++++++-- src/app/mod.rs | 8 ++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/app/file_list.rs b/src/app/file_list.rs index a94f76e..4110247 100644 --- a/src/app/file_list.rs +++ b/src/app/file_list.rs @@ -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 { + // 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::().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 = None; + let mut outline_action: Option = 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; + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index dd73238..4db89ce 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -312,6 +312,9 @@ pub struct App { show_manuscript_settings: bool, /// Whether the Outline window is open. show_outline: bool, + /// Whether to open an outline file from a folder context menu. + /// Contains the workspace-relative path of the outline file to open. + open_outline_file: Option, /// Whether the diff window is open, and what it is showing. show_diff: bool, diff_text: String, @@ -434,6 +437,7 @@ impl App { show_characters: false, show_manuscript_settings: false, show_outline: false, + open_outline_file: None, show_diff: false, diff_text: String::new(), diff_title: String::new(), @@ -589,6 +593,10 @@ impl eframe::App for App { self.diff_window(ctx); } + if self.open_outline_file.is_some() { + self.outline_popup_window(ctx); + } + if self.show_cheatsheet { crate::help::cheatsheet_window(ctx, &mut self.show_cheatsheet); }