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::*;
|
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.
|
/// Horizontal indent per folder level, in points.
|
||||||
const INDENT: f32 = 12.0;
|
const INDENT: f32 = 12.0;
|
||||||
|
|
||||||
@@ -175,6 +208,7 @@ impl App {
|
|||||||
// What the right-click menu picked, applied further down: the
|
// What the right-click menu picked, applied further down: the
|
||||||
// row closure below is still borrowing `self` to draw with.
|
// row closure below is still borrowing `self` to draw with.
|
||||||
let mut action: Option<FileAction> = None;
|
let mut action: Option<FileAction> = None;
|
||||||
|
let mut outline_action: Option<String> = None;
|
||||||
let pointer = ui.input(|i| i.pointer.interact_pos());
|
let pointer = ui.input(|i| i.pointer.interact_pos());
|
||||||
// Filtering happens on the flat list, so folders left with no
|
// Filtering happens on the flat list, so folders left with no
|
||||||
// files simply stop appearing.
|
// files simply stop appearing.
|
||||||
@@ -237,18 +271,28 @@ impl App {
|
|||||||
// widened. The full path is on hover.
|
// widened. The full path is on hover.
|
||||||
let name_w =
|
let name_w =
|
||||||
(row_width - indent - COUNT_W).max(48.0);
|
(row_width - indent - COUNT_W).max(48.0);
|
||||||
if ui
|
let button_response = ui
|
||||||
.add_sized(
|
.add_sized(
|
||||||
[name_w, 20.0],
|
[name_w, 20.0],
|
||||||
egui::Button::new(text)
|
egui::Button::new(text)
|
||||||
.frame(false)
|
.frame(false)
|
||||||
.truncate(),
|
.truncate(),
|
||||||
)
|
)
|
||||||
.on_hover_text(&row.path)
|
.on_hover_text(&row.path);
|
||||||
.clicked()
|
|
||||||
{
|
if button_response.clicked() {
|
||||||
toggled = Some(row.path.clone());
|
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(
|
ui.label(
|
||||||
egui::RichText::new(count.to_string())
|
egui::RichText::new(count.to_string())
|
||||||
.small()
|
.small()
|
||||||
@@ -455,6 +499,10 @@ impl App {
|
|||||||
let ctx = ui.ctx().clone();
|
let ctx = ui.ctx().clone();
|
||||||
self.apply_file_action(action, &ctx);
|
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.separator();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -879,3 +927,49 @@ mod tests {
|
|||||||
assert!(sketch(&[], &[]).is_empty());
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -312,6 +312,9 @@ pub struct App {
|
|||||||
show_manuscript_settings: bool,
|
show_manuscript_settings: bool,
|
||||||
/// Whether the Outline window is open.
|
/// Whether the Outline window is open.
|
||||||
show_outline: bool,
|
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.
|
/// Whether the diff window is open, and what it is showing.
|
||||||
show_diff: bool,
|
show_diff: bool,
|
||||||
diff_text: String,
|
diff_text: String,
|
||||||
@@ -434,6 +437,7 @@ impl App {
|
|||||||
show_characters: false,
|
show_characters: false,
|
||||||
show_manuscript_settings: false,
|
show_manuscript_settings: false,
|
||||||
show_outline: false,
|
show_outline: false,
|
||||||
|
open_outline_file: None,
|
||||||
show_diff: false,
|
show_diff: false,
|
||||||
diff_text: String::new(),
|
diff_text: String::new(),
|
||||||
diff_title: String::new(),
|
diff_title: String::new(),
|
||||||
@@ -589,6 +593,10 @@ impl eframe::App for App {
|
|||||||
self.diff_window(ctx);
|
self.diff_window(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.open_outline_file.is_some() {
|
||||||
|
self.outline_popup_window(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
if self.show_cheatsheet {
|
if self.show_cheatsheet {
|
||||||
crate::help::cheatsheet_window(ctx, &mut self.show_cheatsheet);
|
crate::help::cheatsheet_window(ctx, &mut self.show_cheatsheet);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user