diff --git a/epic_refactor.txt b/epic_refactor.txt deleted file mode 100644 index 383be21..0000000 --- a/epic_refactor.txt +++ /dev/null @@ -1,1339 +0,0 @@ -diff --git a/Cargo.lock b/Cargo.lock -index 751dd12..cdb2db1 100644 ---- a/Cargo.lock -+++ b/Cargo.lock -@@ -1503,6 +1503,7 @@ dependencies = [ - "iced_core", - "log", - "rustc-hash 2.1.1", -+ "tokio", - "wasm-bindgen-futures", - "wasmtimer", - ] -@@ -3656,6 +3657,15 @@ version = "0.1.1" - source = "registry+https://github.com/rust-lang/crates.io-index" - checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -+[[package]] -+name = "tokio" -+version = "1.49.0" -+source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -+dependencies = [ -+ "pin-project-lite", -+] -+ - [[package]] - name = "toml_datetime" - version = "0.7.5+spec-1.1.0" -@@ -4849,6 +4859,7 @@ version = "0.1.0" - dependencies = [ - "iced", - "rfd", -+ "tokio", - "uuid", - ] - -diff --git a/Cargo.toml b/Cargo.toml -index 8daeef9..14fe584 100644 ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -4,6 +4,7 @@ version = "0.1.0" - edition = "2024" - - [dependencies] --iced = { version = "0.14.0", features = ["image", "markdown", "highlighter"] } -+iced = { version = "0.14.0", features = ["image", "markdown", "highlighter", "tokio"] } - rfd = "0.17.1" -+tokio = { version ="1.49.0", features = ["fs"] } - uuid = { version = "1.19.0", features = ["v4"] } -diff --git a/src/components/action_bar.rs b/src/components/action_bar.rs -new file mode 100644 -index 0000000..4dd0ac4 ---- /dev/null -+++ b/src/components/action_bar.rs -@@ -0,0 +1,29 @@ -+use iced::{Element, Length, widget::{container, pick_list, row}}; -+ -+use crate::message::{FileAction, Message, ViewAction}; -+ -+pub fn view( -+ selected_file_action: Option, -+ selected_view_action: Option, -+) -> Element<'static, Message> { -+ -+ let file_menu = pick_list( -+ FileAction::ALL, -+ selected_file_action, -+ Message::FileActionSelected, -+ ) -+ .placeholder("File"); -+ -+ let view_menu = pick_list( -+ ViewAction::ALL, -+ selected_view_action, -+ Message::ViewActionSelected, -+ ) -+ .placeholder("View"); -+ -+ container( -+ row![file_menu, view_menu].spacing(10) -+ ) -+ .width(Length::Fill) -+ .into() -+} -diff --git a/src/components/editor.rs b/src/components/editor.rs -new file mode 100644 -index 0000000..d2a52fd ---- /dev/null -+++ b/src/components/editor.rs -@@ -0,0 +1,32 @@ -+use iced::{ -+ Element, Length, Theme, -+ widget::{container, markdown, row, text_editor}, -+}; -+ -+use crate::{constants, file::File, message::Message, state::Mode}; -+ -+pub fn view<'a>(file: &'a File, mode: Mode, font_size: u32) -> Element<'a, Message> { -+ match mode { -+ Mode::Edit => { -+ let editor = text_editor(file.content()) -+ .size(font_size) -+ .height(Length::Fill) -+ .on_action(Message::Edit); -+ -+ container(row![editor]).height(Length::Fill).into() -+ } -+ Mode::Preview => { -+ let mut style: markdown::Style = Theme::Dark.into(); -+ style.font = constants::CUSTOM_FONT; -+ -+ let settings = markdown::Settings::with_text_size(font_size, style); -+ -+ let markdown_preview = -+ markdown::view(file.markdown(), settings).map(Message::LinkClicked); -+ -+ container(row![markdown_preview]) -+ .height(Length::Fill) -+ .into() -+ } -+ } -+} -diff --git a/src/components/mod.rs b/src/components/mod.rs -new file mode 100644 -index 0000000..6e9ede3 ---- /dev/null -+++ b/src/components/mod.rs -@@ -0,0 +1,4 @@ -+pub mod tabs; -+pub mod editor; -+pub mod status_bar; -+pub mod action_bar; -diff --git a/src/components/status_bar.rs b/src/components/status_bar.rs -new file mode 100644 -index 0000000..a7cbeed ---- /dev/null -+++ b/src/components/status_bar.rs -@@ -0,0 +1,16 @@ -+use iced::widget::{container, row, text}; -+use iced::{Element, Length}; -+use crate::file::File; -+ -+pub fn view(file: &File) -> Element<'_, crate::Message> { -+ let path_text = text(file.path_summary()); -+ let cursor_text = text(file.position_summary()); -+ -+ container( -+ row![cursor_text, path_text] -+ .spacing(20) -+ ) -+ .width(Length::Fill) -+ .padding(5) -+ .into() -+} -diff --git a/src/components/tabs.rs b/src/components/tabs.rs -new file mode 100644 -index 0000000..842cf54 ---- /dev/null -+++ b/src/components/tabs.rs -@@ -0,0 +1,41 @@ -+use crate::file::File; -+use crate::message::{FileAction, Message}; -+use iced::{Background, Border, Theme, border}; -+use iced::{ -+ Element, -+ widget::{button, container, row, scrollable, text}, -+}; -+ -+pub fn view<'a>(files: &'a [File], active_index: usize) -> Element<'a, Message> { -+ let tabs = files.iter().enumerate().map(|(index, file)| { -+ let is_focused = index == active_index; -+ -+ let label = text(file.display_name()); -+ let close_btn = -+ button(text("x")).on_press(Message::FileActionSelected(FileAction::Close(Some(index)))); -+ -+ button(row![label, close_btn].spacing(5)) -+ .on_press(Message::SwitchTab(index)) -+ .padding(5) -+ .style(move |theme: &Theme, status| { -+ let base = button::primary(theme, status); -+ 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 -+ } -+ }) -+ .into() -+ }); -+ -+ scrollable(container(row(tabs).spacing(2))).into() -+} -diff --git a/src/constants.rs b/src/constants.rs -new file mode 100644 -index 0000000..4e4acb9 ---- /dev/null -+++ b/src/constants.rs -@@ -0,0 +1,8 @@ -+use iced::Font; -+ -+pub const CUSTOM_FONT_BYTES: &[u8] = include_bytes!("./fonts/CaskaydiaCoveNFM-Regular.ttf"); -+pub const CUSTOM_FONT: Font = Font::with_name("CaskaydiaCove Nerd Font Mono"); -+pub const DEFAULT_EDITOR_FONT_SIZE: u32 = 16; -+pub const MAX_EDITOR_FONT_SIZE: u32 = 80; -+pub const MIN_EDITOR_FONT_SIZE: u32 = 12; -+pub const ICON_BYTES: &[u8] = include_bytes!("./images/icon.ico"); -diff --git a/src/file.rs b/src/file.rs -index 105303d..fcf1ea4 100644 ---- a/src/file.rs -+++ b/src/file.rs -@@ -1,55 +1,80 @@ --use std::{ -- fs::{read_to_string, write}, -- path::PathBuf, --}; -+use std::path::PathBuf; - --pub fn save_file_to_disk(path: PathBuf, text: String) { -- let save_result = write(path, text); -+use iced::widget::{markdown, text_editor}; - -- match save_result { -- Ok(_) => {}, -- Err(err) => eprintln!("Error: {}", err), -+pub struct File { -+ content: text_editor::Content, -+ path: Option, -+ markdown: Vec, -+} -+ -+impl Default for File { -+ fn default() -> Self { -+ File { -+ content: text_editor::Content::new(), -+ path: None, -+ markdown: Vec::new(), -+ } - } - } - --pub fn load_file_from_disk(path: PathBuf) -> String { -- let read_result = read_to_string(path); -+impl File { -+ pub fn from(content: &str, path: Option) -> Self { -+ let text_editor_content = text_editor::Content::with_text(content); -+ let markdown = markdown::parse(content).collect(); - -- match read_result { -- Ok(contents) => contents, -- Err(_) => String::new(), -+ File { -+ content: text_editor_content, -+ path, -+ markdown, -+ } - } --} - --#[cfg(test)] --mod tests { -- use super::*; -- use std::fs::{self, remove_file}; -+ pub fn content(&self) -> &text_editor::Content { -+ &self.content -+ } - -- #[test] -- fn test_save_file_to_disk() { -- let test_path = PathBuf::from("test_file.txt"); -- let test_content = String::from("Hello, Chat!"); -+ pub fn content_mut(&mut self) -> &mut text_editor::Content { -+ &mut self.content -+ } - -- save_file_to_disk(test_path.clone(), test_content.clone()); -+ pub fn set_content(&mut self, content: &str) { -+ self.content = text_editor::Content::with_text(content); -+ } - -- let saved_content = fs::read_to_string(&test_path).unwrap(); -+ pub fn markdown(&self) -> Vec<&markdown::Item> { -+ self.markdown.iter().collect() -+ } - -- assert_eq!(saved_content, test_content); -+ pub fn update_markdown(&mut self) { -+ self.markdown = markdown::parse(&self.content.text()).collect(); -+ } - -- let _ = remove_file(test_path); -+ pub fn path(&self) -> Option<&PathBuf> { -+ self.path.as_ref() - } - -- #[test] -- fn test_load_file_from_disk() { -- let test_path = PathBuf::from("test_file.txt"); -- let test_content = String::from("Hello, Chat!"); -- let _ = fs::write(test_path.clone(), test_content.clone()); -+ pub fn set_path(&mut self, path: Option) { -+ self.path = path; -+ } - -- let saved_content = load_file_from_disk(test_path.clone()); -+ pub fn display_name(&self) -> &str { -+ self.path -+ .as_deref() -+ .and_then(|p| p.file_name()) -+ .and_then(|n| n.to_str()) -+ .unwrap_or("New file") -+ } - -- assert_eq!(saved_content, test_content); -+ pub fn position_summary(&self) -> String { -+ let pos = self.content.cursor().position; -+ format!("Ln {}, Col {}", pos.line, pos.column) -+ } - -- let _ = remove_file(test_path); -+ pub fn path_summary(&self) -> String { -+ self.path -+ .as_deref() -+ .map(|p| p.to_string_lossy().to_string()) -+ .unwrap_or_default() - } - } -diff --git a/src/handler.rs b/src/handler.rs -new file mode 100644 -index 0000000..110006d ---- /dev/null -+++ b/src/handler.rs -@@ -0,0 +1,81 @@ -+use std::path::PathBuf; -+ -+use crate::io; -+use crate::message::{FileAction, ViewAction}; -+use crate::{Message, state::State}; -+use iced::Task; -+use iced::widget::text_editor; -+ -+pub fn edit(state: &mut State, action: text_editor::Action) -> Task { -+ state.apply_edit(action); -+ Task::none() -+} -+ -+pub fn switch_tab(state: &mut State, index: usize) -> Task { -+ state.switch_tab(index); -+ Task::none() -+} -+ -+pub fn link_clicked(url: String) -> Task { -+ println!("Opening link: {}", url); -+ Task::none() -+} -+ -+pub fn file_action(state: &mut State, action: FileAction) -> Task { -+ match action { -+ FileAction::New => { -+ state.new_file(); -+ Task::none() -+ } -+ FileAction::Close(index) => { -+ if state.close_file(index) { -+ iced::exit() -+ } else { -+ Task::none() -+ } -+ } -+ FileAction::Open => Task::perform(io::open_file(), Message::FileOpened), -+ FileAction::Save => { -+ let (current_path, content) = state.active_file_data(); -+ let path = current_path.cloned(); -+ Task::perform(io::save_file(path, content), Message::FileSaved) -+ } -+ FileAction::SaveAs => { -+ let (_, content) = state.active_file_data(); -+ Task::perform(io::save_file(None, content), Message::FileSaved) -+ } -+ } -+} -+ -+pub fn opened_file(state: &mut State, result: Result<(PathBuf, String), String>) -> Task { -+ match result { -+ Ok((path, content)) => { -+ state.open_file(path, content); -+ } -+ Err(_error) => {} -+ }; -+ -+ Task::none() -+} -+ -+pub fn saved_file(state: &mut State, result: Result) -> Task { -+ match result { -+ Ok(path) => { -+ state.set_active_file_path(path); -+ } -+ Err(_error) => {} -+ }; -+ -+ Task::none() -+} -+ -+pub fn view_action(state: &mut State, action: ViewAction) -> Task { -+ match action { -+ ViewAction::Increase => state.increase_font(), -+ ViewAction::Decrease => state.decrease_font(), -+ ViewAction::Reset => state.reset_font(), -+ ViewAction::TogglePreview => state.toggle_preview(), -+ } -+ -+ Task::none() -+} -diff --git a/src/io.rs b/src/io.rs -new file mode 100644 -index 0000000..c36cf8d ---- /dev/null -+++ b/src/io.rs -@@ -0,0 +1,45 @@ -+use std::path::PathBuf; -+ -+use rfd::AsyncFileDialog; -+use tokio::fs; -+ -+pub async fn open_file() -> Result<(PathBuf, String), String> { -+ let handle_result = AsyncFileDialog::new() -+ .set_directory("/") -+ .pick_file() -+ .await -+ .ok_or(String::from("Dialog cancelled")); -+ -+ match handle_result { -+ Ok(handle) => { -+ let path = handle.path().to_owned(); -+ let content_result = fs::read_to_string(&path).await.map_err(|e| e.to_string()); -+ -+ match content_result { -+ Ok(content) => Ok((path, content)), -+ Err(e) => Err(e), -+ } -+ } -+ Err(e) => Err(e), -+ } -+} -+ -+pub async fn save_file(path: Option, text: String) -> Result { -+ let save_path = match path { -+ Some(p) => p, -+ None => AsyncFileDialog::new() -+ .set_directory("/") -+ .save_file() -+ .await -+ .ok_or(String::from("Dialog cancelled"))? -+ .path() -+ .to_owned(), -+ }; -+ -+ let save_result = fs::write(&save_path, text).await.map_err(|e| e.to_string()); -+ -+ match save_result { -+ Ok(_) => Ok(save_path), -+ Err(err) => Err(err), -+ } -+} -diff --git a/src/key_bindings.rs b/src/key_bindings.rs -new file mode 100644 -index 0000000..f1eb0ab ---- /dev/null -+++ b/src/key_bindings.rs -@@ -0,0 +1,68 @@ -+use iced::keyboard::{self, Key, Modifiers}; -+ -+use crate::message::{FileAction, Message, ViewAction}; -+ -+pub struct Keybinding { -+ key: &'static str, -+ modifiers: Modifiers, -+ message: Message, -+} -+ -+impl Keybinding { -+ pub fn should_handle(&self, key_pressed: &Key, modifiers: &Modifiers) -> bool { -+ let key = keyboard::Key::Character(self.key.into()); -+ modifiers.contains(self.modifiers) && key_pressed == &key -+ } -+ -+ pub fn message(&self) -> Message { -+ self.message.clone() -+ } -+} -+ -+pub const ALL: &[Keybinding] = &[ -+ Keybinding { -+ key: "o", -+ modifiers: Modifiers::CTRL, -+ message: Message::FileActionSelected(FileAction::Open), -+ }, -+ Keybinding { -+ key: "n", -+ modifiers: Modifiers::CTRL, -+ message: Message::FileActionSelected(FileAction::New), -+ }, -+ Keybinding { -+ key: "s", -+ modifiers: Modifiers::CTRL.union(Modifiers::SHIFT), -+ message: Message::FileActionSelected(FileAction::SaveAs), -+ }, -+ Keybinding { -+ key: "s", -+ modifiers: Modifiers::CTRL, -+ message: Message::FileActionSelected(FileAction::Save), -+ }, -+ Keybinding { -+ key: "w", -+ modifiers: Modifiers::CTRL, -+ message: Message::FileActionSelected(FileAction::Close(None)), -+ }, -+ Keybinding { -+ key: "p", -+ modifiers: Modifiers::CTRL, -+ message: Message::ViewActionSelected(ViewAction::TogglePreview), -+ }, -+ Keybinding { -+ key: "=", -+ modifiers: Modifiers::CTRL, -+ message: Message::ViewActionSelected(ViewAction::Increase), -+ }, -+ Keybinding { -+ key: "-", -+ modifiers: Modifiers::CTRL, -+ message: Message::ViewActionSelected(ViewAction::Decrease), -+ }, -+ Keybinding { -+ key: "0", -+ modifiers: Modifiers::CTRL, -+ message: Message::ViewActionSelected(ViewAction::Reset), -+ }, -+]; -diff --git a/src/main.rs b/src/main.rs -index 18a6c3c..8e0b29b 100644 ---- a/src/main.rs -+++ b/src/main.rs -@@ -1,468 +1,75 @@ -+mod components; -+mod constants; - mod file; -- --use std::path::PathBuf; -- --use iced::keyboard::{Key, Modifiers}; --use iced::padding::bottom; --use iced::widget::{ -- button, column, container, markdown, pick_list, row, scrollable, text, text_editor, --}; -+mod handler; -+mod io; -+mod key_bindings; -+mod message; -+mod state; -+ -+use iced::Theme; -+use iced::widget::column; - use iced::window::icon; --use iced::{Background, Border, Element, Subscription, Task, border, event}; --use iced::{Font, Length, Theme}; -+use iced::{Element, Subscription, Task, event}; - use iced::{keyboard, window}; --use rfd::FileDialog; -- --const CUSTOM_FONT: Font = Font::with_name("CaskaydiaCove Nerd Font Mono"); --const DEFAULT_EDITOR_FONT_SIZE: u32 = 16; --const MAX_EDITOR_FONT_SIZE: u32 = 80; --const MIN_EDITOR_FONT_SIZE: u32 = 12; -- --struct File { -- content: text_editor::Content, -- path: Option, -- markdown: Vec, --} -- --impl Default for File { -- fn default() -> Self { -- File { -- content: text_editor::Content::new(), -- path: None, -- markdown: Vec::new(), -- } -- } --} -- --#[derive(Default)] --enum Mode { -- #[default] -- Edit, -- Preview, --} -- --#[derive(Default)] --struct State { -- mode: Mode, -- files: Vec, -- current_file: usize, -- editor_font_size: u32, -- selected_file_action: Option, -- selected_view_action: Option, --} -- --#[derive(Debug, Clone, Copy, PartialEq, Eq)] --enum FileAction { -- New, -- Save, -- SaveAs, -- Open, -- Close(Option), --} -- --impl FileAction { -- const ALL: &'static [FileAction] = &[ -- FileAction::New, -- FileAction::Save, -- FileAction::SaveAs, -- FileAction::Open, -- FileAction::Close(None), -- ]; --} -- --impl std::fmt::Display for FileAction { -- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -- match self { -- FileAction::New => write!(f, "New file"), -- FileAction::Save => write!(f, "Save"), -- FileAction::SaveAs => write!(f, "Save as... "), -- FileAction::Open => write!(f, "Open"), -- FileAction::Close(_) => write!(f, "Close"), -- } -- } --} -- --#[derive(Debug, Clone, Copy, PartialEq, Eq)] --enum ViewAction { -- Increase, -- Decrease, -- Reset, -- TogglePreview, --} -- --impl ViewAction { -- const ALL: &'static [ViewAction] = &[ -- ViewAction::Increase, -- ViewAction::Decrease, -- ViewAction::Reset, -- ViewAction::TogglePreview, -- ]; --} -- --impl std::fmt::Display for ViewAction { -- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -- match self { -- ViewAction::Decrease => write!(f, "Decrease font"), -- ViewAction::Increase => write!(f, "Increase font"), -- ViewAction::Reset => write!(f, "Reset font"), -- ViewAction::TogglePreview => write!(f, "Toggle preview"), -- } -- } --} -- --#[derive(Debug, Clone)] --enum Message { -- Edit(text_editor::Action), -- FileActionSelected(FileAction), -- ViewActionSelected(ViewAction), -- SwitchTab(usize), -- LinkClicked(String), --} -- --// TODO: Can we at least make --// theme configurable --// and then maybe even define --// complete custom and/or use --// existing as starter --fn theme(_state: &State) -> Theme { -- Theme::Dark --} -- --fn view(state: &State) -> Element<'_, Message> { -- let mut tab_row = row![]; - -- for (file_index, file) in state.files.iter().enumerate() { -- let file_name = if let Some(p) = &file.path { -- p.file_name() -- .expect("unable to get file name") -- .to_str() -- .expect("unable to get file name") -- } else { -- "New file" -- }; -+use crate::message::Message; -+use crate::state::State; - -- let tab_button_text = text(file_name).wrapping(text::Wrapping::None); -- let delete_button = button(text("x")).on_press(Message::FileActionSelected( -- FileAction::Close(Some(file_index)), -- )); -- -- let tab_button = button(row![tab_button_text, delete_button]) -- .style(move |theme: &Theme, status| { -- let base = button::primary(theme, status); -- let is_focused = state.current_file == file_index; -- 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_index)); -- -- tab_row = tab_row.push(tab_button); -- } -- -- let tabs = scrollable(container(tab_row).padding(bottom(10))).direction( -- scrollable::Direction::Horizontal(scrollable::Scrollbar::new()), -- ); -- -- let file_menu = pick_list( -- FileAction::ALL, -- state.selected_file_action, -- Message::FileActionSelected, -- ) -- .placeholder("File"); -- -- let view_menu = pick_list( -- ViewAction::ALL, -- state.selected_view_action, -- Message::ViewActionSelected, -- ) -- .placeholder("View"); -- -- let action_bar = container(row![file_menu, view_menu].spacing(5)); -- -- let current_file = &state.files[state.current_file]; -- -- let editor = text_editor(¤t_file.content) -- .size(state.editor_font_size) -- .height(Length::Fill) -- .on_action(Message::Edit); -- -- let mut markdown_styles: markdown::Style = Theme::Dark.into(); -- markdown_styles.font = CUSTOM_FONT; -- -- let markdown_settings = -- markdown::Settings::with_text_size(state.editor_font_size, markdown_styles); -- -- let markdown_preview = -- markdown::view(¤t_file.markdown, markdown_settings).map(Message::LinkClicked); -- -- let preview_container = container(row![markdown_preview]).height(Length::Fill); -- let editor_container = container(row![editor]).height(Length::Fill); -- -- let cursor_position = current_file.content.cursor().position; -- -- let cursor_display_text = format!( -- "Ln {}, Col {}", -- cursor_position.line, cursor_position.column -- ); -- let cursor_text = text(cursor_display_text); -- -- let file_path_display_text = match ¤t_file.path { -- Some(path) => path.to_string_lossy().to_string(), -- None => String::new(), -- }; -- -- let file_path_text = text(file_path_display_text); -- -- let status_bar = container(row![file_path_text, cursor_text].spacing(10)); -- -- let main = match state.mode { -- Mode::Edit => editor_container, -- Mode::Preview => preview_container, -- }; -- -- container(column![tabs, action_bar, main, status_bar]) -- .padding(10) -- .into() --} -- --fn save_file(path: Option, text: String) -> Option { -- let mut save_path = path.clone(); -- -- if path.is_none() { -- save_path = FileDialog::new().set_directory("/").save_file(); -- } -- -- match save_path { -- Some(p) => { -- let sp = p.clone(); -- file::save_file_to_disk(sp, text); -- Some(p) -- } -- None => None, -- } --} -- --fn save_file_as(text: String) -> Option { -- let files = FileDialog::new().set_directory("/").save_file(); -- -- match files { -- Some(path) => { -- file::save_file_to_disk(path.clone(), text); -- Some(path) -- } -- None => None, -- } -+pub fn main() -> iced::Result { -+ iced::application(boot, update, view) -+ .subscription(subscription) -+ .font(constants::CUSTOM_FONT_BYTES) -+ .theme(theme) -+ .settings(iced::Settings { -+ default_font: constants::CUSTOM_FONT, -+ ..Default::default() -+ }) -+ .window(window::Settings { -+ icon: Some( -+ icon::from_file_data(constants::ICON_BYTES, None).expect("Failed to load icon"), -+ ), -+ ..window::Settings::default() -+ }) -+ .run() - } - --fn open_file() -> (Option, String) { -- // TODO: Allow opening multiple files -- let file = FileDialog::new().set_directory("/").pick_file(); -- -- match file { -- Some(path) => { -- let content = file::load_file_from_disk(path.clone()); -- (Some(path), content) -- } -- None => (None, String::new()), -- } -+fn boot() -> State { -+ state::State::new() - } - - fn update(state: &mut State, message: Message) -> Task { - match message { -- Message::Edit(action) => { -- let current_file = &mut state.files[state.current_file]; -- current_file.content.perform(action); -- current_file.markdown = markdown::parse(¤t_file.content.text()).collect(); -- } -- Message::FileActionSelected(action) => { -- state.selected_file_action = None; -- -- match action { -- FileAction::SaveAs => { -- println!("We are here!"); -- let current_file = &mut state.files[state.current_file]; -- let path = save_file_as(current_file.content.text()); -- -- if path.is_some() { -- current_file.path = path; -- } -- } -- FileAction::Open => { -- let (path, content) = open_file(); -- -- if let Some(opened_path) = &path { -- for (file_index, file) in state.files.iter_mut().enumerate() { -- if let Some(existing_path) = &file.path -- && opened_path == existing_path -- { -- file.content = text_editor::Content::with_text(&content); -- state.current_file = file_index; -- return Task::none(); -- } -- } -- -- let opened_file = File { -- path, -- content: text_editor::Content::with_text(&content), -- markdown: markdown::parse(&content).collect(), -- }; -- -- state.files.push(opened_file); -- state.current_file = state.files.len() - 1; -- } -- } -- FileAction::Save => { -- let current_file = &mut state.files[state.current_file]; -- let path = save_file(current_file.path.clone(), current_file.content.text()); -- current_file.path = path; -- } -- FileAction::New => { -- let default_file = File::default(); -- -- state.files.push(default_file); -- state.current_file = state.files.len() - 1; -- } -- FileAction::Close(idx) => { -- let idx_to_close = match idx { -- Some(i) => i, -- None => state.current_file, -- }; -- -- if state.files.len() == 1 { -- return iced::exit(); -- } -- -- if state.files.len() - 1 == idx_to_close { -- state.current_file = state.files.len() - 2; -- state.files.remove(idx_to_close); -- return Task::none(); -- } -- -- state.current_file -= 1; -- state.files.remove(idx_to_close); -- } -- } -- } -- Message::ViewActionSelected(action) => { -- state.selected_view_action = None; -- -- match action { -- ViewAction::Increase => { -- if state.editor_font_size >= MAX_EDITOR_FONT_SIZE { -- return Task::none(); -- } -- -- state.editor_font_size += 2; -- } -- ViewAction::Decrease => { -- if state.editor_font_size <= MIN_EDITOR_FONT_SIZE { -- return Task::none(); -- } -- -- state.editor_font_size -= 2; -- } -- ViewAction::Reset => { -- state.editor_font_size = DEFAULT_EDITOR_FONT_SIZE; -- } -- // TODO: Carry previous mode when toggling -- // so that if we already in preview we can -- // just put the user back where they were -- ViewAction::TogglePreview => match state.mode { -- Mode::Edit => state.mode = Mode::Preview, -- Mode::Preview => state.mode = Mode::Edit, -- }, -- } -- } -- Message::SwitchTab(file_id) => { -- state.current_file = file_id; -- } -- Message::LinkClicked(link) => { -- print!("Link clicked: {}", link); -- } -+ Message::Edit(action) => handler::edit(state, action), -+ Message::SwitchTab(index) => handler::switch_tab(state, index), -+ Message::LinkClicked(url) => handler::link_clicked(url), -+ Message::FileActionSelected(action) => handler::file_action(state, action), -+ Message::ViewActionSelected(action) => handler::view_action(state, action), -+ Message::FileOpened(result) => handler::opened_file(state, result), -+ Message::FileSaved(result) => handler::saved_file(state, result), - } -- -- Task::none() - } - --struct Keybinding { -- key: &'static str, -- modifiers: Modifiers, -- message: Message, --} -- --impl Keybinding { -- fn should_handle(&self, key_pressed: &Key, modifiers: &Modifiers) -> bool { -- let key = keyboard::Key::Character(self.key.into()); -- modifiers.contains(self.modifiers) && key_pressed == &key -- } -+fn view(state: &State) -> Element<'_, Message> { -+ let current_file = state.active_file(); - -- const ALL: &'static [Keybinding] = &[ -- Keybinding { -- key: "o", -- modifiers: Modifiers::CTRL, -- message: Message::FileActionSelected(FileAction::Open), -- }, -- Keybinding { -- key: "n", -- modifiers: Modifiers::CTRL, -- message: Message::FileActionSelected(FileAction::New), -- }, -- Keybinding { -- key: "s", -- modifiers: Modifiers::CTRL.union(Modifiers::SHIFT), -- message: Message::FileActionSelected(FileAction::SaveAs), -- }, -- Keybinding { -- key: "s", -- modifiers: Modifiers::CTRL, -- message: Message::FileActionSelected(FileAction::Save), -- }, -- Keybinding { -- key: "w", -- modifiers: Modifiers::CTRL, -- message: Message::FileActionSelected(FileAction::Close(None)), -- }, -- Keybinding { -- key: "p", -- modifiers: Modifiers::CTRL, -- message: Message::ViewActionSelected(ViewAction::TogglePreview), -- }, -- Keybinding { -- key: "=", -- modifiers: Modifiers::CTRL, -- message: Message::ViewActionSelected(ViewAction::Increase), -- }, -- Keybinding { -- key: "-", -- modifiers: Modifiers::CTRL, -- message: Message::ViewActionSelected(ViewAction::Decrease), -- }, -- Keybinding { -- key: "0", -- modifiers: Modifiers::CTRL, -- message: Message::ViewActionSelected(ViewAction::Reset), -- }, -- ]; -+ column![ -+ components::tabs::view(state.files(), state.current_file_index()), -+ components::action_bar::view(state.selected_file_action(), state.selected_view_action()), -+ components::editor::view(current_file, state.mode(), state.font_size()), -+ components::status_bar::view(current_file), -+ ] -+ .padding(10) -+ .into() - } - - fn subscription(_state: &State) -> Subscription { - event::listen_with(|e, _status, _win| -> Option { - match e { - iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => { -- for vb in Keybinding::ALL { -+ for vb in key_bindings::ALL { - if vb.should_handle(&key, &modifiers) { -- return Some(vb.message.clone()); -+ return Some(vb.message()); - } - } - -@@ -473,34 +80,6 @@ fn subscription(_state: &State) -> Subscription { - }) - } - --fn boot() -> State { -- let default_file = File::default(); -- -- let files = vec![default_file]; -- -- State { -- files, -- current_file: 0, -- editor_font_size: DEFAULT_EDITOR_FONT_SIZE, -- ..Default::default() -- } --} -- --pub fn main() -> iced::Result { -- iced::application(boot, update, view) -- .subscription(subscription) -- .font(include_bytes!("./fonts/CaskaydiaCoveNFM-Regular.ttf")) -- .theme(theme) -- .settings(iced::Settings { -- default_font: CUSTOM_FONT, -- ..Default::default() -- }) -- .window(window::Settings { -- icon: Some( -- icon::from_file_data(include_bytes!("./images/icon.ico"), None) -- .expect("Failed to load icon"), -- ), -- ..window::Settings::default() -- }) -- .run() -+fn theme(_state: &State) -> Theme { -+ Theme::Dark - } -diff --git a/src/message.rs b/src/message.rs -new file mode 100644 -index 0000000..e05f1dc ---- /dev/null -+++ b/src/message.rs -@@ -0,0 +1,73 @@ -+use std::{fmt::Display, path::PathBuf}; -+ -+use iced::widget::text_editor; -+ -+#[derive(Debug, Clone, Copy, PartialEq, Eq)] -+pub enum FileAction { -+ New, -+ Save, -+ SaveAs, -+ Open, -+ Close(Option), -+} -+ -+impl FileAction { -+ pub const ALL: &'static [FileAction] = &[ -+ FileAction::New, -+ FileAction::Save, -+ FileAction::SaveAs, -+ FileAction::Open, -+ FileAction::Close(None), -+ ]; -+} -+ -+impl Display for FileAction { -+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -+ match self { -+ FileAction::New => write!(f, "New file"), -+ FileAction::Save => write!(f, "Save"), -+ FileAction::SaveAs => write!(f, "Save as... "), -+ FileAction::Open => write!(f, "Open"), -+ FileAction::Close(_) => write!(f, "Close"), -+ } -+ } -+} -+ -+#[derive(Debug, Clone, Copy, PartialEq, Eq)] -+pub enum ViewAction { -+ Increase, -+ Decrease, -+ Reset, -+ TogglePreview, -+} -+ -+impl ViewAction { -+ pub const ALL: &'static [ViewAction] = &[ -+ ViewAction::Increase, -+ ViewAction::Decrease, -+ ViewAction::Reset, -+ ViewAction::TogglePreview, -+ ]; -+} -+ -+impl Display for ViewAction { -+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -+ match self { -+ ViewAction::Decrease => write!(f, "Decrease font"), -+ ViewAction::Increase => write!(f, "Increase font"), -+ ViewAction::Reset => write!(f, "Reset font"), -+ ViewAction::TogglePreview => write!(f, "Toggle preview"), -+ } -+ } -+} -+ -+#[derive(Debug, Clone)] -+pub enum Message { -+ Edit(text_editor::Action), -+ FileActionSelected(FileAction), -+ ViewActionSelected(ViewAction), -+ SwitchTab(usize), -+ LinkClicked(String), -+ FileOpened(Result<(PathBuf, String), String>), -+ FileSaved(Result), -+} -diff --git a/src/state.rs b/src/state.rs -new file mode 100644 -index 0000000..7ca7197 ---- /dev/null -+++ b/src/state.rs -@@ -0,0 +1,147 @@ -+use std::path::PathBuf; -+ -+use iced::widget::text_editor; -+ -+use crate::{constants, file}; -+use crate::message::{FileAction, ViewAction}; -+ -+#[derive(Default, Copy, Clone)] -+pub enum Mode { -+ #[default] -+ Edit, -+ Preview, -+} -+ -+#[derive(Default)] -+pub struct State { -+ mode: Mode, -+ files: Vec, -+ current_file: usize, -+ editor_font_size: u32, -+ selected_file_action: Option, -+ selected_view_action: Option, -+} -+ -+impl State { -+ pub fn new() -> Self { -+ let default_file = file::File::default(); -+ -+ Self { -+ files: vec![default_file], -+ current_file: 0, -+ editor_font_size: constants::DEFAULT_EDITOR_FONT_SIZE, -+ ..Default::default() -+ } -+ } -+ -+ pub fn apply_edit(&mut self, action: text_editor::Action) { -+ self.files[self.current_file].content_mut().perform(action); -+ } -+ -+ pub fn new_file(&mut self) { -+ self.files.push(file::File::default()); -+ self.current_file = self.files.len() - 1; -+ } -+ -+ pub fn close_file(&mut self, index: Option) -> bool { -+ let index = index.unwrap_or(self.current_file); -+ -+ if self.files.len() <= 1 { -+ return true; -+ } -+ -+ self.files.remove(index); -+ -+ if self.current_file >= self.files.len() { -+ self.current_file = self.files.len().saturating_sub(1); -+ } else if index < self.current_file { -+ self.current_file -= 1; -+ } -+ -+ false -+ } -+ -+ pub fn open_file(&mut self, path: PathBuf, content: String) { -+ if let Some(index) = self.files.iter().position(|f| f.path() == Some(&path)) { -+ self.files[index].set_content(&content); -+ self.current_file = index; -+ return; -+ } -+ -+ let opened_file = file::File::from(&content, Some(path)); -+ -+ self.files.push(opened_file); -+ self.current_file = self.files.len() - 1; -+ } -+ -+ pub fn switch_tab(&mut self, index: usize) { -+ if index < self.files.len() { -+ self.current_file = index; -+ } -+ } -+ -+ pub fn active_file_data(&self) -> (Option<&PathBuf>, String) { -+ let file = &self.files[self.current_file]; -+ (file.path(), file.content().text()) -+ } -+ -+ pub fn set_active_file_path(&mut self, path: PathBuf) { -+ if let Some(file) = self.files.get_mut(self.current_file) { -+ file.set_path(Some(path)); -+ } -+ } -+ -+ pub fn increase_font(&mut self) { -+ if self.editor_font_size < constants::MAX_EDITOR_FONT_SIZE { -+ self.editor_font_size += 2; -+ } -+ } -+ -+ pub fn decrease_font(&mut self) { -+ if self.editor_font_size > constants::MIN_EDITOR_FONT_SIZE { -+ self.editor_font_size -= 2; -+ } -+ } -+ -+ pub fn reset_font(&mut self) { -+ self.editor_font_size = constants::DEFAULT_EDITOR_FONT_SIZE; -+ } -+ -+ pub fn toggle_preview(&mut self) { -+ match self.mode { -+ Mode::Edit => { -+ self.files[self.current_file].update_markdown(); -+ self.mode = Mode::Preview; -+ } -+ Mode::Preview => self.mode = Mode::Edit, -+ } -+ } -+ -+ pub fn files(&self) -> &[file::File] { -+ &self.files -+ } -+ -+ pub fn current_file_index(&self) -> usize { -+ self.current_file -+ } -+ -+ pub fn active_file(&self) -> &file::File { -+ &self.files[self.current_file] -+ } -+ -+ pub fn mode(&self) -> Mode { -+ self.mode -+ } -+ -+ pub fn font_size(&self) -> u32 { -+ self.editor_font_size -+ } -+ -+ pub fn selected_file_action(&self) -> Option { -+ self.selected_file_action -+ } -+ -+ pub fn selected_view_action(&self) -> Option { -+ self.selected_view_action -+ } -+}