feat: refactor state into hashmap + add new file action with keybind

This commit is contained in:
Stevan Freeborn
2026-01-15 07:02:21 -06:00
parent 599aacaf3b
commit 573724901f
3 changed files with 165 additions and 43 deletions
Generated
+2
View File
@@ -3570,6 +3570,7 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [ dependencies = [
"getrandom",
"js-sys", "js-sys",
"serde_core", "serde_core",
"wasm-bindgen", "wasm-bindgen",
@@ -4606,6 +4607,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"iced", "iced",
"rfd", "rfd",
"uuid",
] ]
[[package]] [[package]]
+1
View File
@@ -6,3 +6,4 @@ edition = "2024"
[dependencies] [dependencies]
iced = { version = "0.14.0", features = ["image"] } iced = { version = "0.14.0", features = ["image"] }
rfd = "0.17.1" rfd = "0.17.1"
uuid = { version = "1.19.0", features = ["v4"] }
+162 -43
View File
@@ -3,21 +3,26 @@ mod file;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use iced::Element; use iced::padding::bottom;
use iced::widget::text_editor::{Binding, KeyPress}; use iced::widget::text_editor::{Binding, KeyPress};
use iced::widget::{button, column, container, pick_list, row, text, text_editor}; use iced::widget::{button, column, container, pick_list, row, scrollable, text, text_editor};
use iced::window::icon; use iced::window::icon;
use iced::{Background, Border, Element, border};
use iced::{Font, Length, Theme}; use iced::{Font, Length, Theme};
use iced::{keyboard, window}; use iced::{keyboard, window};
use rfd::FileDialog; use rfd::FileDialog;
use uuid::Uuid;
use crate::file::load_file_from_disk;
const CUSTOM_FONT: Font = Font::with_name("CaskaydiaCove Nerd Font Mono"); const CUSTOM_FONT: Font = Font::with_name("CaskaydiaCove Nerd Font Mono");
const DEFAULT_EDITOR_FONT_SIZE: u32 = 16; const DEFAULT_EDITOR_FONT_SIZE: u32 = 16;
const MAX_EDITOR_FONT_SIZE: u32 = 80; const MAX_EDITOR_FONT_SIZE: u32 = 80;
const MIN_EDITOR_FONT_SIZE: u32 = 12; const MIN_EDITOR_FONT_SIZE: u32 = 12;
// TODO: Need to handle that when user
// cancels file action it is causing
// the currently selected path to be
// emptied out
// TODO: Need to reconsider the way // TODO: Need to reconsider the way
// we are holding on to files. Maybe // we are holding on to files. Maybe
// vec is not best // vec is not best
@@ -34,30 +39,64 @@ const MIN_EDITOR_FONT_SIZE: u32 = 12;
// TODO: Opening another window // TODO: Opening another window
struct File {
id: Uuid,
content: text_editor::Content,
path: Option<PathBuf>,
}
impl Default for File {
fn default() -> Self {
File {
id: Uuid::new_v4(),
content: text_editor::Content::new(),
path: None,
}
}
}
// TODO: Chat says I need to update
// the files map to be a map between
// an id and an index
// Then files need to be held in;
// list
#[derive(Default)] #[derive(Default)]
struct State { struct State {
files: HashMap<Option<PathBuf>, bool>, files: HashMap<Uuid, File>,
file_path: Option<PathBuf>, current_file: Uuid,
content: text_editor::Content,
editor_font_size: u32, editor_font_size: u32,
selected_file_action: Option<FileAction>, selected_file_action: Option<FileAction>,
selected_view_action: Option<ViewAction>, selected_view_action: Option<ViewAction>,
} }
// fn get_current_file(mut files: &HashMap<Uuid, File>, current_file: &Uuid) -> &mut File {
// files
// .get_mut(current_file)
// .expect("current file id not present in files map")
// }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FileAction { enum FileAction {
New,
Save, Save,
SaveAs, SaveAs,
Open, Open,
} }
impl FileAction { impl FileAction {
const ALL: &'static [FileAction] = &[FileAction::Save, FileAction::SaveAs, FileAction::Open]; const ALL: &'static [FileAction] = &[
FileAction::New,
FileAction::Save,
FileAction::SaveAs,
FileAction::Open,
];
} }
impl std::fmt::Display for FileAction { impl std::fmt::Display for FileAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
FileAction::New => write!(f, "New file"),
FileAction::Save => write!(f, "Save"), FileAction::Save => write!(f, "Save"),
FileAction::SaveAs => write!(f, "Save as... "), FileAction::SaveAs => write!(f, "Save as... "),
FileAction::Open => write!(f, "Open"), FileAction::Open => write!(f, "Open"),
@@ -95,7 +134,7 @@ enum Message {
Edit(text_editor::Action), Edit(text_editor::Action),
FileActionSelected(FileAction), FileActionSelected(FileAction),
ViewActionSelected(ViewAction), ViewActionSelected(ViewAction),
SwitchTab(PathBuf), SwitchTab(Uuid),
} }
fn theme(_state: &State) -> Theme { fn theme(_state: &State) -> Theme {
@@ -106,20 +145,45 @@ fn view(state: &State) -> Element<'_, Message> {
let mut tab_row = row![]; let mut tab_row = row![];
for file in &state.files { for file in &state.files {
if let Some(p) = &file.0 { let file_name = if let Some(p) = &file.1.path {
let file_name = p.file_name() p.file_name()
.expect("unable to get file name") .expect("unable to get file name")
.to_str() .to_str()
.expect("unable to get file name"); .expect("unable to get file name")
} else {
let button_text = text(file_name).wrapping(text::Wrapping::None); "New file"
let button = button(button_text).on_press(Message::SwitchTab(p.clone())); };
tab_row = tab_row.push(button); let button_text = text(file_name).wrapping(text::Wrapping::None);
}
let button = button(button_text)
.style(|theme: &Theme, status| {
let base = button::primary(theme, status);
let current_file = state.files.get(&state.current_file).expect("problem");
let is_focused = current_file.id == file.0.clone();
let button_background = if is_focused {
base.background
} else {
Some(Background::Color(theme.palette().background))
};
button::Style {
background: button_background,
border: Border {
radius: border::radius(0).top_left(10).top_right(10),
..base.border
},
..base
}
})
.on_press(Message::SwitchTab(file.0.clone()));
tab_row = tab_row.push(button);
} }
let tabs = container(tab_row); let tabs = scrollable(container(tab_row).padding(bottom(10))).direction(
scrollable::Direction::Horizontal(scrollable::Scrollbar::new()),
);
let file_menu = pick_list( let file_menu = pick_list(
FileAction::ALL, FileAction::ALL,
@@ -137,22 +201,26 @@ fn view(state: &State) -> Element<'_, Message> {
let action_bar = container(row![file_menu, view_menu].spacing(5)); let action_bar = container(row![file_menu, view_menu].spacing(5));
let editor = text_editor(&state.content) let current_file = state.files.get(&state.current_file).expect("problem");
let editor = text_editor(&current_file.content)
.size(state.editor_font_size) .size(state.editor_font_size)
.height(Length::Fill) .height(Length::Fill)
.on_action(Message::Edit) .on_action(Message::Edit)
.key_binding(|key_press: KeyPress| { .key_binding(|key_press: KeyPress| {
let n = keyboard::Key::Character("n".into());
let s = keyboard::Key::Character("s".into()); let s = keyboard::Key::Character("s".into());
let o = keyboard::Key::Character("o".into()); let o = keyboard::Key::Character("o".into());
let minus = keyboard::Key::Character("-".into()); let minus = keyboard::Key::Character("-".into());
let equals = keyboard::Key::Character("=".into()); let equals = keyboard::Key::Character("=".into());
let zero = keyboard::Key::Character("0".into()); let zero = keyboard::Key::Character("0".into());
let is_open = key_press.modifiers.command() && key_press.key == o; let is_new = key_press.modifiers.command() && key_press.key == n;
let is_save = key_press.modifiers.command() && key_press.key == s; let is_save = key_press.modifiers.command() && key_press.key == s;
let is_save_as = let is_save_as =
key_press.modifiers.command() && key_press.modifiers.shift() && key_press.key == s; key_press.modifiers.command() && key_press.modifiers.shift() && key_press.key == s;
let is_open = key_press.modifiers.command() && key_press.key == o;
let is_increase_font = key_press.modifiers.command() && key_press.key == equals; let is_increase_font = key_press.modifiers.command() && key_press.key == equals;
let is_decrease_font = key_press.modifiers.command() && key_press.key == minus; let is_decrease_font = key_press.modifiers.command() && key_press.key == minus;
let is_reset_font = key_press.modifiers.command() && key_press.key == zero; let is_reset_font = key_press.modifiers.command() && key_press.key == zero;
@@ -193,19 +261,28 @@ fn view(state: &State) -> Element<'_, Message> {
))); )));
} }
if is_new {
return Some(Binding::Custom(Message::FileActionSelected(
FileAction::New,
)));
}
text_editor::Binding::from_key_press(key_press) text_editor::Binding::from_key_press(key_press)
}); });
let editor_container = container(row![editor]).height(Length::Fill); let editor_container = container(row![editor]).height(Length::Fill);
let cursor_position = state.content.cursor().position; let cursor_position = current_file.content.cursor().position;
let cursor_display_text = format!( let cursor_display_text = format!(
"Ln {}, Col {}", "Ln {}, Col {}",
cursor_position.line, cursor_position.column cursor_position.line, cursor_position.column
); );
let cursor_text = text(cursor_display_text); let cursor_text = text(cursor_display_text);
let file_path_display_text = match &state.file_path { let current_file = state.files.get(&state.current_file).expect("problem");
let file_path_display_text = match &current_file.path {
Some(path) => path.to_string_lossy().to_string(), Some(path) => path.to_string_lossy().to_string(),
None => String::new(), None => String::new(),
}; };
@@ -214,7 +291,7 @@ fn view(state: &State) -> Element<'_, Message> {
let status_bar = container(row![file_path_text, cursor_text].spacing(10)); let status_bar = container(row![file_path_text, cursor_text].spacing(10));
container(column![tabs, action_bar, editor_container, status_bar].spacing(10)) container(column![tabs, action_bar, editor_container, status_bar])
.padding(10) .padding(10)
.into() .into()
} }
@@ -248,44 +325,80 @@ fn save_file_as(text: String) -> Option<PathBuf> {
} }
} }
fn open_file() -> (PathBuf, String) { fn open_file() -> (Option<PathBuf>, String) {
let file = FileDialog::new().set_directory("/").pick_file(); let file = FileDialog::new().set_directory("/").pick_file();
match file { match file {
Some(path) => { Some(path) => {
let content = file::load_file_from_disk(path.clone()); let content = file::load_file_from_disk(path.clone());
(path, content) (Some(path), content)
} }
None => (PathBuf::new(), String::new()), None => (None, String::new()),
} }
} }
fn update(state: &mut State, message: Message) { fn update(state: &mut State, message: Message) {
match message { match message {
Message::Edit(action) => { Message::Edit(action) => {
state.content.perform(action); let current_file = state.files.get_mut(&state.current_file).expect("problem");
current_file.content.perform(action);
} }
Message::FileActionSelected(action) => { Message::FileActionSelected(action) => {
state.selected_file_action = None; state.selected_file_action = None;
match action { match action {
FileAction::SaveAs => { FileAction::SaveAs => {
let path = save_file_as(state.content.text()); let current_file = state.files.get_mut(&state.current_file).expect("problem");
state.file_path = path.clone(); let path = save_file_as(current_file.content.text());
state.files.insert(path, true); current_file.path = path;
} }
FileAction::Open => { FileAction::Open => {
let (path, content) = open_file(); let (path, content) = open_file();
state.content = text_editor::Content::with_text(&content);
match &path {
let path_option = Some(path); Some(opened_path) => {
state.file_path = path_option.clone(); // TODO: Chat thinks this is stupid
state.files.insert(path_option, true); // and I agree. There is probably
// a way to have some sort of map
// between id and paths
for file in state.files.iter_mut() {
match &file.1.path {
Some(existing_path) => {
if opened_path == existing_path {
file.1.content =
text_editor::Content::with_text(&content);
state.current_file = file.1.id;
return;
}
}
None => {}
}
}
let opened_file = File {
path: path,
content: text_editor::Content::with_text(&content),
..Default::default()
};
let opened_file_id = opened_file.id;
state.files.insert(opened_file.id, opened_file);
state.current_file = opened_file_id;
}
None => {}
}
} }
FileAction::Save => { FileAction::Save => {
let path = save_file(state.file_path.clone(), state.content.text()); let current_file = state.files.get_mut(&state.current_file).expect("problem");
state.file_path = path.clone(); let path = save_file(current_file.path.clone(), current_file.content.text());
state.files.insert(path, true); current_file.path = path;
}
FileAction::New => {
let default_file = File::default();
let default_file_id = default_file.id;
state.files.insert(default_file.id, default_file);
state.current_file = default_file_id;
} }
} }
} }
@@ -310,16 +423,22 @@ fn update(state: &mut State, message: Message) {
ViewAction::ResetFont => state.editor_font_size = DEFAULT_EDITOR_FONT_SIZE, ViewAction::ResetFont => state.editor_font_size = DEFAULT_EDITOR_FONT_SIZE,
} }
} }
Message::SwitchTab(path) => { Message::SwitchTab(file_id) => {
let file_content = load_file_from_disk(path.clone()); state.current_file = file_id;
state.content = text_editor::Content::with_text(&file_content);
state.file_path = Some(path);
} }
} }
} }
fn boot() -> State { fn boot() -> State {
let mut files: HashMap<Uuid, File> = HashMap::new();
let default_file = File::default();
let default_file_id = default_file.id;
files.insert(default_file.id, default_file);
State { State {
files: files,
current_file: default_file_id,
editor_font_size: DEFAULT_EDITOR_FONT_SIZE, editor_font_size: DEFAULT_EDITOR_FONT_SIZE,
..Default::default() ..Default::default()
} }