Refactor
This commit is contained in:
parent
fc165694eb
commit
49dcb4a84e
5 changed files with 268 additions and 897 deletions
290
src/main.rs
290
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<FontSystem> = 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<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
|
|
@ -54,110 +45,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Project {
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||
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<PathBuf>,
|
||||
attrs: Attrs<'static>,
|
||||
editor: Mutex<ViEditor<'static>>,
|
||||
}
|
||||
|
||||
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<Project>,
|
||||
|
|
@ -344,154 +231,7 @@ impl cosmic::Application for App {
|
|||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
/*
|
||||
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]);
|
||||
|
||||
|
|
|
|||
130
src/menu.rs
Normal file
130
src/menu.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
622
src/menu_list.rs
622
src/menu_list.rs
|
|
@ -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: &<Self as StyleSheet>::Style) -> Appearance;
|
||||
|
||||
/// Produces the hovered [`Appearance`] of a pick list.
|
||||
fn hovered(&self, style: &<Self as StyleSheet>::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<Owned = Vec<T>>,
|
||||
Renderer: text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
on_selected: Box<dyn Fn(T) -> Message + 'a>,
|
||||
options: Cow<'a, [T]>,
|
||||
placeholder: Option<String>,
|
||||
selected: Option<T>,
|
||||
width: Length,
|
||||
padding: Padding,
|
||||
text_size: Option<u16>,
|
||||
font: Renderer::Font,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> MenuList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: ToString + Eq,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Renderer: text::Renderer,
|
||||
<Renderer as text::Renderer>::Font: std::default::Default,
|
||||
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::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<Cow<'a, [T]>>,
|
||||
selected: Option<T>,
|
||||
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<String>) -> 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<P: Into<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<<Renderer::Theme as StyleSheet>::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> for MenuList<'a, T, Message, Renderer>
|
||||
where
|
||||
T: Clone + ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State<T>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::<T>::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<f32>,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
shell,
|
||||
self.on_selected.as_ref(),
|
||||
self.selected.as_ref(),
|
||||
&self.options,
|
||||
|| tree.state.downcast_mut::<State<T>>(),
|
||||
)
|
||||
}
|
||||
|
||||
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<overlay::Element<'b, Message, Renderer>> {
|
||||
let state = tree.state.downcast_mut::<State<T>>();
|
||||
|
||||
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<MenuList<'a, T, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
T: Clone + ToString + Eq + 'static,
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::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<T> {
|
||||
menu: menu::State,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: bool,
|
||||
hovered_option: Option<usize>,
|
||||
last_selection: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> State<T> {
|
||||
/// 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<T> Default for State<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`MenuList`].
|
||||
pub fn layout<Renderer, T>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
padding: Padding,
|
||||
text_size: Option<u16>,
|
||||
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<T>,
|
||||
) -> 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<Item = &'a T>,
|
||||
) -> 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<T>,
|
||||
padding: Padding,
|
||||
text_size: Option<u16>,
|
||||
font: Renderer::Font,
|
||||
options: &'a [T],
|
||||
on_selected: &'a dyn Fn(T) -> Message,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Option<overlay::Element<'a, Message, Renderer>>
|
||||
where
|
||||
T: Clone + ToString,
|
||||
Message: 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
|
||||
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::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<T, Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
padding: Padding,
|
||||
text_size: Option<u16>,
|
||||
font: &Renderer::Font,
|
||||
placeholder: Option<&str>,
|
||||
selected: Option<&T>,
|
||||
style: &<Renderer::Theme as StyleSheet>::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,
|
||||
});
|
||||
}
|
||||
}
|
||||
28
src/project.rs
Normal file
28
src/project.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub struct Project {
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
95
src/tab.rs
Normal file
95
src/tab.rs
Normal file
|
|
@ -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<PathBuf>,
|
||||
attrs: Attrs<'static>,
|
||||
pub editor: Mutex<ViEditor<'static>>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue