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:
2026-09-13 10:47:11 -05:00
parent 9f575c7c7e
commit cea944779e
2 changed files with 106 additions and 4 deletions
+98 -4
View File
@@ -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;
}
}
}
+8
View File
@@ -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<String>,
/// 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);
}