From 49dcb4a84e62099fe26bcfa65c4b1a5f64c9f250 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 26 Oct 2023 10:15:09 -0600 Subject: [PATCH] Refactor --- src/main.rs | 290 ++-------------------- src/menu.rs | 130 ++++++++++ src/menu_list.rs | 622 ----------------------------------------------- src/project.rs | 28 +++ src/tab.rs | 95 ++++++++ 5 files changed, 268 insertions(+), 897 deletions(-) create mode 100644 src/menu.rs delete mode 100644 src/menu_list.rs create mode 100644 src/project.rs create mode 100644 src/tab.rs diff --git a/src/main.rs b/src/main.rs index a6fe00a..7db8fb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,46 +4,37 @@ use cosmic::{ app::{message, Command, Core, Settings}, executor, iced::{ - widget::{column, horizontal_rule, horizontal_space, row, text}, - Alignment, Length, Limits, - }, - theme, - widget::{ - self, button, icon, - menu::{ItemHeight, ItemWidth, MenuBar, MenuTree}, - segmented_button, view_switcher, + widget::{column, text}, + Length, Limits, }, + widget::{self, icon, segmented_button, view_switcher}, ApplicationExt, Element, }; -use cosmic_text::{ - Attrs, Buffer, Edit, FontSystem, Metrics, SyntaxEditor, SyntaxSystem, ViEditor, ViMode, -}; +use cosmic_text::{FontSystem, SyntaxSystem, ViMode}; use std::{ - env, fs, io, + env, path::{Path, PathBuf}, sync::Mutex, }; -use self::menu_list::MenuList; -mod menu_list; +use self::menu::menu_bar; +mod menu; + +use self::project::Project; +mod project; + +use self::tab::Tab; +mod tab; use self::text_box::text_box; mod text_box; +//TODO: re-use iced FONT_SYSTEM lazy_static::lazy_static! { static ref FONT_SYSTEM: Mutex = Mutex::new(FontSystem::new()); static ref SYNTAX_SYSTEM: SyntaxSystem = SyntaxSystem::new(); } -static FONT_SIZES: &'static [Metrics] = &[ - Metrics::new(10.0, 14.0), // Caption - Metrics::new(14.0, 20.0), // Body - Metrics::new(20.0, 28.0), // Title 4 - Metrics::new(24.0, 32.0), // Title 3 - Metrics::new(28.0, 36.0), // Title 2 - Metrics::new(32.0, 44.0), // Title 1 -]; - fn main() -> Result<(), Box> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); @@ -54,110 +45,6 @@ fn main() -> Result<(), Box> { Ok(()) } -pub struct Project { - path: PathBuf, - name: String, -} - -impl Project { - pub fn new>(path: P) -> io::Result { - let path = fs::canonicalize(path)?; - let name = path - .file_name() - .ok_or(io::Error::new( - io::ErrorKind::Other, - format!("Path {:?} has no file name", path), - ))? - .to_str() - .ok_or(io::Error::new( - io::ErrorKind::Other, - format!("Path {:?} is not valid UTF-8", path), - ))? - .to_string(); - Ok(Self { path, name }) - } -} - -pub struct Tab { - path_opt: Option, - attrs: Attrs<'static>, - editor: Mutex>, -} - -impl Tab { - pub fn new() -> Self { - let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace); - - let editor = SyntaxEditor::new( - Buffer::new(&mut FONT_SYSTEM.lock().unwrap(), FONT_SIZES[1 /* Body */]), - &SYNTAX_SYSTEM, - "base16-eighties.dark", - ) - .unwrap(); - - let mut editor = ViEditor::new(editor); - editor.set_passthrough(false); - - Self { - path_opt: None, - attrs, - editor: Mutex::new(editor), - } - } - - pub fn open(&mut self, path: PathBuf) { - let mut editor = self.editor.lock().unwrap(); - let mut font_system = FONT_SYSTEM.lock().unwrap(); - let mut editor = editor.borrow_with(&mut font_system); - match editor.load_text(&path, self.attrs) { - Ok(()) => { - log::info!("opened '{}'", path.display()); - self.path_opt = Some(path); - } - Err(err) => { - log::error!("failed to open '{}': {}", path.display(), err); - self.path_opt = None; - } - } - } - - pub fn save(&mut self) { - if let Some(path) = &self.path_opt { - let editor = self.editor.lock().unwrap(); - let mut text = String::new(); - for line in editor.buffer().lines.iter() { - text.push_str(line.text()); - text.push('\n'); - } - match fs::write(path, text) { - Ok(()) => { - log::info!("saved '{}'", path.display()); - } - Err(err) => { - log::error!("failed to save '{}': {}", path.display(), err); - } - } - } else { - log::warn!("tab has no path yet"); - } - } - - pub fn title(&self) -> String { - //TODO: show full title when there is a conflict - if let Some(path) = &self.path_opt { - match path.file_name() { - Some(file_name_os) => match file_name_os.to_str() { - Some(file_name) => file_name.to_string(), - None => format!("{}", path.display()), - }, - None => format!("{}", path.display()), - } - } else { - "New document".to_string() - } - } -} - pub struct App { core: Core, projects: Vec, @@ -344,154 +231,7 @@ impl cosmic::Application for App { } fn view(&self) -> Element { - /* - let menu_bar = row![ - MenuList::new( - vec![ - "New file", - "New window", - "Open file...", - "Save", - "Save as..." - ], - None, - |item| { - match item { - "Open" => Message::OpenDialog, - "Save" => Message::Save, - _ => Message::Todo, - } - } - ) - .padding(8) - .placeholder("File"), - MenuList::new(vec!["Todo"], None, |_| Message::Todo).placeholder("Edit"), - MenuList::new(vec!["Todo"], None, |_| Message::Todo).placeholder("View"), - MenuList::new(vec!["Todo"], None, |_| Message::Todo).placeholder("Help"), - ] - .align_items(Alignment::Start) - .padding(4) - .spacing(16); - */ - - //TODO: port to libcosmic - let menu_root = |label| { - button(label) - .padding([4, 12]) - .style(theme::Button::MenuRoot) - }; - let menu_folder = |label| { - button( - row![text(label), horizontal_space(Length::Fill), text(">")] - .align_items(Alignment::Center), - ) - .height(Length::Fixed(32.0)) - .padding([4, 12]) - .width(Length::Fill) - .style(theme::Button::MenuItem) - }; - let menu_item = |label, message| { - MenuTree::new( - button(row![label].align_items(Alignment::Center)) - .height(Length::Fixed(32.0)) - .on_press(message) - .padding([4, 12]) - .width(Length::Fill) - .style(theme::Button::MenuItem), - ) - }; - let menu_key = |label, key, message| { - MenuTree::new( - button( - row![text(label), horizontal_space(Length::Fill), text(key)] - .align_items(Alignment::Center), - ) - .height(Length::Fixed(32.0)) - .on_press(message) - .padding([4, 12]) - .style(theme::Button::MenuItem), - ) - }; - let menu_bar: Element<_> = MenuBar::new(vec![ - MenuTree::with_children( - menu_root("File"), - vec![ - menu_key("New file", "Ctrl + N", Message::New), - menu_key("New window", "Ctrl + Shift + N", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_key("Open file...", "Ctrl + O", Message::OpenDialog), - MenuTree::with_children( - menu_folder("Open recent"), - vec![menu_item("TODO", Message::Todo)], - ), - MenuTree::new(horizontal_rule(1)), - menu_key("Save", "Ctrl + S", Message::Save), - menu_key("Save as...", "Ctrl + Shift + S", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_item("Revert all changes", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_item("Document statistics...", Message::Todo), - menu_item("Document type...", Message::Todo), - menu_item("Encoding...", Message::Todo), - menu_item("Print", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_key("Quit", "Ctrl + Q", Message::Todo), - ], - ), - MenuTree::with_children( - menu_root("Edit"), - vec![ - menu_key("Undo", "Ctrl + Z", Message::Todo), - menu_key("Redo", "Ctrl + Shift + Z", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_key("Cut", "Ctrl + X", Message::Todo), - menu_key("Copy", "Ctrl + C", Message::Todo), - menu_key("Paste", "Ctrl + V", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_key("Find", "Ctrl + F", Message::Todo), - menu_key("Replace", "Ctrl + H", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_item("Spell check...", Message::Todo), - ], - ), - MenuTree::with_children( - menu_root("View"), - vec![ - MenuTree::with_children( - menu_folder("Indentation"), - vec![ - menu_item("Automatic indentation", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_item("Tab width: 1", Message::Todo), - menu_item("Tab width: 2", Message::Todo), - menu_item("Tab width: 4", Message::Todo), - menu_item("Tab width: 8", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_item("Convert indentation to spaces", Message::Todo), - menu_item("Convert indentation to tabs", Message::Todo), - ], - ), - MenuTree::new(horizontal_rule(1)), - menu_item("Word wrap", Message::Todo), - menu_item("Show line numbers", Message::Todo), - menu_item("Highlight current line", Message::Todo), - menu_item("Syntax highlighting...", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_key("Settings...", "Ctrl + ,", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_item("Keyboard shortcuts...", Message::Todo), - MenuTree::new(horizontal_rule(1)), - menu_item("About COSMIC Text Editor", Message::Todo), - ], - ), - ]) - .cross_offset(0) - .item_height(ItemHeight::Dynamic(40)) - .item_width(ItemWidth::Uniform(240)) - .main_offset(0) - .padding(8) - .spacing(4.0) - .into(); + let menu_bar = menu_bar(); let mut tab_column = widget::column::with_capacity(3).padding([0, 16]); diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 0000000..17090fb --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::{ + //TODO: export in cosmic::widget + iced::{widget::horizontal_rule, Alignment, Length}, + theme, + widget::{ + self, button, horizontal_space, + menu::{ItemHeight, ItemWidth, MenuBar, MenuTree}, + text, + }, + Element, +}; + +use crate::Message; + +pub fn menu_bar<'a>() -> Element<'a, Message> { + //TODO: port to libcosmic + let menu_root = |label| { + button(label) + .padding([4, 12]) + .style(theme::Button::MenuRoot) + }; + + macro_rules! menu_button { + ($($x:expr),+ $(,)?) => ( + button( + widget::Row::with_children( + vec![$(Element::from($x)),+] + ) + .align_items(Alignment::Center) + ) + .height(Length::Fixed(32.0)) + .padding([4, 16]) + .width(Length::Fill) + .style(theme::Button::MenuItem) + ); + } + + let menu_folder = |label| menu_button!(text(label), horizontal_space(Length::Fill), text(">")); + + let menu_item = |label, message| MenuTree::new(menu_button!(text(label)).on_press(message)); + + let menu_key = |label, key, message| { + MenuTree::new( + menu_button!(text(label), horizontal_space(Length::Fill), text(key)).on_press(message), + ) + }; + + MenuBar::new(vec![ + MenuTree::with_children( + menu_root("File"), + vec![ + menu_key("New file", "Ctrl + N", Message::New), + menu_key("New window", "Ctrl + Shift + N", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_key("Open file...", "Ctrl + O", Message::OpenDialog), + MenuTree::with_children( + menu_folder("Open recent"), + vec![menu_item("TODO", Message::Todo)], + ), + MenuTree::new(horizontal_rule(1)), + menu_key("Save", "Ctrl + S", Message::Save), + menu_key("Save as...", "Ctrl + Shift + S", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_item("Revert all changes", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_item("Document statistics...", Message::Todo), + menu_item("Document type...", Message::Todo), + menu_item("Encoding...", Message::Todo), + menu_item("Print", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_key("Quit", "Ctrl + Q", Message::Todo), + ], + ), + MenuTree::with_children( + menu_root("Edit"), + vec![ + menu_key("Undo", "Ctrl + Z", Message::Todo), + menu_key("Redo", "Ctrl + Shift + Z", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_key("Cut", "Ctrl + X", Message::Todo), + menu_key("Copy", "Ctrl + C", Message::Todo), + menu_key("Paste", "Ctrl + V", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_key("Find", "Ctrl + F", Message::Todo), + menu_key("Replace", "Ctrl + H", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_item("Spell check...", Message::Todo), + ], + ), + MenuTree::with_children( + menu_root("View"), + vec![ + MenuTree::with_children( + menu_folder("Indentation"), + vec![ + menu_item("Automatic indentation", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_item("Tab width: 1", Message::Todo), + menu_item("Tab width: 2", Message::Todo), + menu_item("Tab width: 4", Message::Todo), + menu_item("Tab width: 8", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_item("Convert indentation to spaces", Message::Todo), + menu_item("Convert indentation to tabs", Message::Todo), + ], + ), + MenuTree::new(horizontal_rule(1)), + menu_item("Word wrap", Message::Todo), + menu_item("Show line numbers", Message::Todo), + menu_item("Highlight current line", Message::Todo), + menu_item("Syntax highlighting...", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_key("Settings...", "Ctrl + ,", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_item("Keyboard shortcuts...", Message::Todo), + MenuTree::new(horizontal_rule(1)), + menu_item("About COSMIC Text Editor", Message::Todo), + ], + ), + ]) + .cross_offset(0) + .item_height(ItemHeight::Dynamic(40)) + .item_width(ItemWidth::Uniform(240)) + .main_offset(0) + .padding(8) + .spacing(4.0) + .into() +} diff --git a/src/menu_list.rs b/src/menu_list.rs deleted file mode 100644 index 5bcea71..0000000 --- a/src/menu_list.rs +++ /dev/null @@ -1,622 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -use cosmic::{ - iced::{ - alignment, - event::{self, Event}, - keyboard, mouse, overlay, - overlay::menu::{self, Menu}, - touch, - widget::container, - widget::scrollable, - Background, Color, Element, Length, Padding, Rectangle, Size, - }, - iced_core::{ - clipboard::Clipboard, - layout::{self, Layout}, - renderer, - text::{self, LineHeight, Shaping, Text}, - widget::tree::{self, Tree}, - Shell, Widget, - }, -}; -use std::borrow::Cow; - -/// The appearance of a pick list. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The text [`Color`] of the pick list. - pub text_color: Color, - /// The placeholder [`Color`] of the pick list. - pub placeholder_color: Color, - /// The [`Background`] of the pick list. - pub background: Background, - /// The border radius of the pick list. - pub border_radius: f32, - /// The border width of the pick list. - pub border_width: f32, - /// The border color of the pick list. - pub border_color: Color, -} - -/// A set of rules that dictate the style of a container. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default + Clone; - - /// Produces the active [`Appearance`] of a pick list. - fn active(&self, style: &::Style) -> Appearance; - - /// Produces the hovered [`Appearance`] of a pick list. - fn hovered(&self, style: &::Style) -> Appearance; -} - -impl StyleSheet for cosmic::theme::Theme { - type Style = (); - - fn active(&self, _style: &()) -> Appearance { - let cosmic = &self.cosmic().primary.component; - - Appearance { - text_color: cosmic.on.into(), - background: Color::TRANSPARENT.into(), - placeholder_color: cosmic.on.into(), - border_radius: 8.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - - fn hovered(&self, style: &()) -> Appearance { - let cosmic = &self.cosmic().primary.component; - - Appearance { - background: Background::Color(cosmic.hover.into()), - ..self.active(style) - } - } -} - -/// A widget for selecting a single value from a list of options. -#[allow(missing_debug_implementations)] -pub struct MenuList<'a, T, Message, Renderer> -where - [T]: ToOwned>, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - on_selected: Box Message + 'a>, - options: Cow<'a, [T]>, - placeholder: Option, - selected: Option, - width: Length, - padding: Padding, - text_size: Option, - font: Renderer::Font, - style: ::Style, -} - -impl<'a, T: 'a, Message, Renderer> MenuList<'a, T, Message, Renderer> -where - T: ToString + Eq, - [T]: ToOwned>, - Renderer: text::Renderer, - ::Font: std::default::Default, - Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet, - ::Style: From<::Style>, -{ - /// The default padding of a [`MenuList`]. - pub const DEFAULT_PADDING: Padding = Padding::new(8.0); - - /// Creates a new [`MenuList`] with the given list of options, the current - /// selected value, and the message to produce when an option is selected. - pub fn new( - options: impl Into>, - selected: Option, - on_selected: impl Fn(T) -> Message + 'a, - ) -> Self { - Self { - on_selected: Box::new(on_selected), - options: options.into(), - placeholder: None, - selected, - width: Length::Shrink, - text_size: None, - padding: Self::DEFAULT_PADDING, - font: Default::default(), - style: Default::default(), - } - } - - /// Sets the placeholder of the [`MenuList`]. - pub fn placeholder(mut self, placeholder: impl Into) -> Self { - self.placeholder = Some(placeholder.into()); - self - } - - /// Sets the width of the [`MenuList`]. - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - /// Sets the [`Padding`] of the [`MenuList`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`MenuList`]. - pub fn text_size(mut self, size: u16) -> Self { - self.text_size = Some(size); - self - } - - /// Sets the font of the [`MenuList`]. - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; - self - } - - /// Sets the style of the [`MenuList`]. - pub fn style(mut self, style: impl Into<::Style>) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, T: 'a, Message, Renderer> Widget for MenuList<'a, T, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet, - ::Style: From<::Style>, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(State::::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.text_size, - &self.font, - self.placeholder.as_deref(), - &self.options, - ) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) -> event::Status { - update( - event, - layout, - cursor_position, - shell, - self.on_selected.as_ref(), - self.selected.as_ref(), - &self.options, - || tree.state.downcast_mut::>(), - ) - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } - - fn draw( - &self, - _tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _viewport: &Rectangle, - ) { - draw( - renderer, - theme, - layout, - cursor_position, - self.padding, - self.text_size, - &self.font, - self.placeholder.as_deref(), - self.selected.as_ref(), - &self.style, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let state = tree.state.downcast_mut::>(); - - overlay( - renderer, - layout, - state, - self.padding, - self.text_size, - self.font.clone(), - &self.options, - &self.on_selected, - self.style.clone(), - ) - } -} - -impl<'a, T: 'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet, - ::Style: From<::Style>, -{ - fn from(menu_list: MenuList<'a, T, Message, Renderer>) -> Self { - Self::new(menu_list) - } -} - -/// The local state of a [`MenuList`]. -#[derive(Debug)] -pub struct State { - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option, - last_selection: Option, -} - -impl State { - /// Creates a new [`State`] for a [`MenuList`]. - pub fn new() -> Self { - Self { - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: bool::default(), - hovered_option: Option::default(), - last_selection: Option::default(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -/// Computes the layout of a [`MenuList`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - text_size: Option, - font: &Renderer::Font, - placeholder: Option<&str>, - options: &[T], -) -> layout::Node -where - Renderer: text::Renderer, - T: ToString, -{ - use std::f32; - - let limits = limits.width(width).height(Length::Shrink).pad(padding); - - let text_size = text_size.unwrap_or_else(|| renderer.default_size() as u16); - - let max_width = match width { - Length::Shrink => { - let measure = |label: &str| -> u32 { - let size = renderer.measure( - label, - text_size as f32, - LineHeight::default(), - font.clone(), - Size::new(f32::INFINITY, f32::INFINITY), - Shaping::Advanced, - ); - - size.width.round() as u32 - }; - - placeholder.map(measure).unwrap_or(100) - } - _ => 0, - }; - - let size = { - let intrinsic = Size::new(max_width as f32, f32::from(text_size)); - - limits.resolve(intrinsic).pad(padding) - }; - - layout::Node::new(size) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`MenuList`] -/// accordingly. -pub fn update<'a, T, Message>( - event: Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - shell: &mut Shell<'_, Message>, - on_selected: &dyn Fn(T) -> Message, - selected: Option<&T>, - options: &[T], - state: impl FnOnce() -> &'a mut State, -) -> event::Status -where - T: PartialEq + Clone + 'a, -{ - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - let event_status = if state.is_open { - // Event wasn't processed by overlay, so cursor was clicked either outside it's - // bounds or on the drop-down, either way we close the overlay. - state.is_open = false; - - event::Status::Captured - } else if cursor_position.is_over(layout.bounds()) { - state.is_open = true; - state.hovered_option = options.iter().position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; - - if let Some(last_selection) = state.last_selection.take() { - shell.publish((on_selected)(last_selection)); - - state.is_open = false; - - event::Status::Captured - } else { - event_status - } - } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) => { - let state = state(); - - if state.keyboard_modifiers.command() - && cursor_position.is_over(layout.bounds()) - && !state.is_open - { - fn find_next<'a, T: PartialEq>( - selected: &'a T, - mut options: impl Iterator, - ) -> Option<&'a T> { - let _ = options.find(|&option| option == selected); - - options.next() - } - - let next_option = if y < 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter()) - } else { - options.first() - } - } else if y > 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter().rev()) - } else { - options.last() - } - } else { - None - }; - - if let Some(next_option) = next_option { - shell.publish((on_selected)(next_option.clone())); - } - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = modifiers; - - event::Status::Ignored - } - _ => event::Status::Ignored, - } -} - -/// Returns the current [`mouse::Interaction`] of a [`MenuList`]. -pub fn mouse_interaction(layout: Layout<'_>, cursor_position: mouse::Cursor) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor_position.is_over(bounds); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} - -/// Returns the current overlay of a [`MenuList`]. -pub fn overlay<'a, T, Message, Renderer>( - renderer: &Renderer, - layout: Layout<'_>, - state: &'a mut State, - padding: Padding, - text_size: Option, - font: Renderer::Font, - options: &'a [T], - on_selected: &'a dyn Fn(T) -> Message, - style: ::Style, -) -> Option> -where - T: Clone + ToString, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet, - ::Style: From<::Style>, -{ - if state.is_open { - let bounds = layout.bounds(); - - let text_size = text_size.unwrap_or_else(|| renderer.default_size() as u16); - - let width = { - let measure = |label: &str| -> u32 { - let size = renderer.measure( - label, - text_size as f32, - LineHeight::default(), - font.clone(), - Size::new(f32::INFINITY, f32::INFINITY), - Shaping::Advanced, - ); - - size.width.round() as u32 - }; - - let labels = options.iter().map(ToString::to_string); - - let labels_width = labels.map(|label| measure(&label)).max().unwrap_or(100); - - labels_width as f32 + padding.left + padding.right - }; - - let menu = Menu::new( - &mut state.menu, - options, - &mut state.hovered_option, - |option| { - state.is_open = false; - - (on_selected)(option) - }, - None, - ) - .width(width) - .padding(padding) - .font(font) - .style(style) - .text_size(text_size); - - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None - } -} - -/// Draws a [`MenuList`]. -pub fn draw( - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - padding: Padding, - text_size: Option, - font: &Renderer::Font, - placeholder: Option<&str>, - selected: Option<&T>, - style: &::Style, -) where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, - T: ToString, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor_position.is_over(bounds); - let is_selected = selected.is_some(); - - let style = if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius.into(), - }, - style.background, - ); - - let label = selected.map(ToString::to_string); - - if let Some(label) = label.as_deref().or(placeholder) { - let text_size = text_size.map_or_else(|| renderer.default_size(), f32::from); - - renderer.fill_text(Text { - content: label, - bounds: Rectangle { - x: bounds.x + f32::from(padding.left), - y: bounds.center_y() - text_size / 2.0, - width: bounds.width - f32::from(padding.horizontal()), - height: text_size, - }, - size: text_size, - line_height: LineHeight::default(), - color: if is_selected { - style.text_color - } else { - style.placeholder_color - }, - font: font.clone(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Advanced, - }); - } -} diff --git a/src/project.rs b/src/project.rs new file mode 100644 index 0000000..07b13e0 --- /dev/null +++ b/src/project.rs @@ -0,0 +1,28 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +pub struct Project { + path: PathBuf, + name: String, +} + +impl Project { + pub fn new>(path: P) -> io::Result { + let path = fs::canonicalize(path)?; + let name = path + .file_name() + .ok_or(io::Error::new( + io::ErrorKind::Other, + format!("Path {:?} has no file name", path), + ))? + .to_str() + .ok_or(io::Error::new( + io::ErrorKind::Other, + format!("Path {:?} is not valid UTF-8", path), + ))? + .to_string(); + Ok(Self { path, name }) + } +} diff --git a/src/tab.rs b/src/tab.rs new file mode 100644 index 0000000..30af24e --- /dev/null +++ b/src/tab.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_text::{Attrs, Buffer, Edit, Metrics, SyntaxEditor, ViEditor}; +use std::{fs, path::PathBuf, sync::Mutex}; + +use crate::{FONT_SYSTEM, SYNTAX_SYSTEM}; + +static FONT_SIZES: &'static [Metrics] = &[ + Metrics::new(10.0, 14.0), // Caption + Metrics::new(14.0, 20.0), // Body + Metrics::new(20.0, 28.0), // Title 4 + Metrics::new(24.0, 32.0), // Title 3 + Metrics::new(28.0, 36.0), // Title 2 + Metrics::new(32.0, 44.0), // Title 1 +]; + +pub struct Tab { + pub path_opt: Option, + attrs: Attrs<'static>, + pub editor: Mutex>, +} + +impl Tab { + pub fn new() -> Self { + let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace); + + let editor = SyntaxEditor::new( + Buffer::new(&mut FONT_SYSTEM.lock().unwrap(), FONT_SIZES[1 /* Body */]), + &SYNTAX_SYSTEM, + "base16-eighties.dark", + ) + .unwrap(); + + let mut editor = ViEditor::new(editor); + editor.set_passthrough(false); + + Self { + path_opt: None, + attrs, + editor: Mutex::new(editor), + } + } + + pub fn open(&mut self, path: PathBuf) { + let mut editor = self.editor.lock().unwrap(); + let mut font_system = FONT_SYSTEM.lock().unwrap(); + let mut editor = editor.borrow_with(&mut font_system); + match editor.load_text(&path, self.attrs) { + Ok(()) => { + log::info!("opened '{}'", path.display()); + self.path_opt = Some(path); + } + Err(err) => { + log::error!("failed to open '{}': {}", path.display(), err); + self.path_opt = None; + } + } + } + + pub fn save(&mut self) { + if let Some(path) = &self.path_opt { + let editor = self.editor.lock().unwrap(); + let mut text = String::new(); + for line in editor.buffer().lines.iter() { + text.push_str(line.text()); + text.push('\n'); + } + match fs::write(path, text) { + Ok(()) => { + log::info!("saved '{}'", path.display()); + } + Err(err) => { + log::error!("failed to save '{}': {}", path.display(), err); + } + } + } else { + log::warn!("tab has no path yet"); + } + } + + pub fn title(&self) -> String { + //TODO: show full title when there is a conflict + if let Some(path) = &self.path_opt { + match path.file_name() { + Some(file_name_os) => match file_name_os.to_str() { + Some(file_name) => file_name.to_string(), + None => format!("{}", path.display()), + }, + None => format!("{}", path.display()), + } + } else { + "New document".to_string() + } + } +}