This commit is contained in:
Jeremy Soller 2023-10-26 10:15:09 -06:00
parent fc165694eb
commit 49dcb4a84e
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
5 changed files with 268 additions and 897 deletions

View file

@ -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
View 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()
}

View file

@ -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
View 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
View 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()
}
}
}