diff --git a/CHANGELOG.md b/CHANGELOG.md index 232aff2..07bf1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Removed + +- Remove editor-libcosmic, see cosmic-edit instead + ## [0.11.0] - 2024-02-07 ### Added diff --git a/editor-libcosmic.sh b/editor-libcosmic.sh deleted file mode 100755 index b85b85c..0000000 --- a/editor-libcosmic.sh +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT OR Apache-2.0 - -RUST_LOG="cosmic_text=debug,editor_libcosmic=debug" cargo run --release --package editor-libcosmic -- "$@" diff --git a/examples/editor-libcosmic/Cargo.toml b/examples/editor-libcosmic/Cargo.toml deleted file mode 100644 index 82e2ec0..0000000 --- a/examples/editor-libcosmic/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "editor-libcosmic" -version = "0.1.0" -authors = ["Jeremy Soller "] -edition = "2021" -license = "MIT OR Apache-2.0" -publish = false - -[dependencies] -cosmic-text = { path = "../../", features = ["syntect", "vi"] } -env_logger = "0.10" -fontdb = "0.13" -lazy_static = "1.4" -log = "0.4" - -[dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic" -rev = "e3f30a1" -#path = "../../../libcosmic" - -[dependencies.rfd] -version = "0.11" -#TODO: iced portal -#default-features = false -#features = ["xdg-portal"] - -[features] -default = [] -vi = [] diff --git a/examples/editor-libcosmic/src/main.rs b/examples/editor-libcosmic/src/main.rs deleted file mode 100644 index 9b66e6d..0000000 --- a/examples/editor-libcosmic/src/main.rs +++ /dev/null @@ -1,397 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use cosmic::{ - iced::{ - self, - widget::{column, horizontal_space, pick_list, row}, - Alignment, Application, Color, Command, Length, - }, - settings, - theme::{self, Theme, ThemeType}, - widget::{button, text, toggler}, - Element, -}; -use cosmic_text::{ - Align, Attrs, AttrsList, Buffer, Edit, FontSystem, Metrics, SyntaxEditor, SyntaxSystem, Wrap, -}; -use std::{env, fmt, fs, path::PathBuf, sync::Mutex}; - -use self::text_box::text_box; -mod text_box; - -lazy_static::lazy_static! { - static ref FONT_SYSTEM: Mutex = Mutex::new(FontSystem::new()); - static ref SYNTAX_SYSTEM: SyntaxSystem = SyntaxSystem::new(); -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FontSize { - Caption, - Body, - Title4, - Title3, - Title2, - Title1, -} - -impl FontSize { - pub fn all() -> &'static [Self] { - &[ - Self::Caption, - Self::Body, - Self::Title4, - Self::Title3, - Self::Title2, - Self::Title1, - ] - } - - pub fn to_metrics(self) -> Metrics { - match self { - Self::Caption => Metrics::new(10.0, 14.0), // Caption - Self::Body => Metrics::new(14.0, 20.0), // Body - Self::Title4 => Metrics::new(20.0, 28.0), // Title 4 - Self::Title3 => Metrics::new(24.0, 32.0), // Title 3 - Self::Title2 => Metrics::new(28.0, 36.0), // Title 2 - Self::Title1 => Metrics::new(32.0, 44.0), // Title 1 - } - } -} - -impl fmt::Display for FontSize { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Caption => write!(f, "Caption"), - Self::Body => write!(f, "Body"), - Self::Title4 => write!(f, "Title 4"), - Self::Title3 => write!(f, "Title 3"), - Self::Title2 => write!(f, "Title 2"), - Self::Title1 => write!(f, "Title 1"), - } - } -} - -static WRAP_MODE: &[Wrap] = &[Wrap::None, Wrap::Glyph, Wrap::Word]; - -fn main() -> cosmic::iced::Result { - env_logger::init(); - - let mut settings = settings(); - settings.window.min_size = Some((400, 100)); - Window::run(settings) -} - -pub struct Window { - theme: Theme, - path_opt: Option, - attrs: Attrs<'static>, - font_size: FontSize, - editor: Mutex>, -} - -#[allow(dead_code)] -#[derive(Clone, Copy, Debug)] -pub enum Message { - Open, - Save, - Bold(bool), - Italic(bool), - Monospaced(bool), - FontSizeChanged(FontSize), - WrapChanged(Wrap), - AlignmentChanged(Align), - ThemeChanged(&'static str), -} - -impl Window { - 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; - } - } - } -} - -impl Application for Window { - type Executor = iced::executor::Default; - type Flags = (); - type Message = Message; - type Theme = Theme; - - fn new(_flags: ()) -> (Self, Command) { - let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace); - - let editor = SyntaxEditor::new( - Buffer::new( - &mut FONT_SYSTEM.lock().unwrap(), - FontSize::Body.to_metrics(), - ), - &SYNTAX_SYSTEM, - "base16-eighties.dark", - ) - .unwrap(); - - let mut editor = cosmic_text::ViEditor::new(editor); - editor.set_passthrough(cfg!(feature = "vi")); - - update_attrs(&mut editor, attrs); - - let mut window = Window { - theme: Theme::dark(), - font_size: FontSize::Body, - path_opt: None, - attrs, - editor: Mutex::new(editor), - }; - if let Some(arg) = env::args().nth(1) { - window.open(PathBuf::from(arg)); - } - (window, Command::none()) - } - - fn theme(&self) -> Theme { - self.theme.clone() - } - - fn title(&self) -> String { - if let Some(path) = &self.path_opt { - format!( - "COSMIC Text - {} - {}", - FONT_SYSTEM.lock().unwrap().locale(), - path.display() - ) - } else { - format!("COSMIC Text - {}", FONT_SYSTEM.lock().unwrap().locale()) - } - } - - fn update(&mut self, message: Message) -> iced::Command { - match message { - Message::Open => { - if let Some(path) = rfd::FileDialog::new().pick_file() { - self.open(path); - } - } - Message::Save => { - if let Some(path) = &self.path_opt { - let editor = self.editor.lock().unwrap(); - let mut text = String::new(); - editor.with_buffer(|buffer| { - for line in 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); - } - } - } - } - Message::Bold(bold) => { - self.attrs = self.attrs.weight(if bold { - cosmic_text::Weight::BOLD - } else { - cosmic_text::Weight::NORMAL - }); - - let mut editor = self.editor.lock().unwrap(); - update_attrs(&mut *editor, self.attrs); - } - Message::Italic(italic) => { - self.attrs = self.attrs.style(if italic { - cosmic_text::Style::Italic - } else { - cosmic_text::Style::Normal - }); - - let mut editor = self.editor.lock().unwrap(); - update_attrs(&mut *editor, self.attrs); - } - Message::Monospaced(monospaced) => { - self.attrs = self.attrs.family(if monospaced { - cosmic_text::Family::Monospace - } else { - cosmic_text::Family::SansSerif - }); - - let mut editor = self.editor.lock().unwrap(); - update_attrs(&mut *editor, self.attrs); - } - Message::FontSizeChanged(font_size) => { - self.font_size = font_size; - let mut editor = self.editor.lock().unwrap(); - editor - .borrow_with(&mut FONT_SYSTEM.lock().unwrap()) - .with_buffer_mut(|buffer| buffer.set_metrics(font_size.to_metrics())); - } - Message::WrapChanged(wrap) => { - let mut editor = self.editor.lock().unwrap(); - editor - .borrow_with(&mut FONT_SYSTEM.lock().unwrap()) - .with_buffer_mut(|buffer| buffer.set_wrap(wrap)); - } - Message::AlignmentChanged(align) => { - let mut editor = self.editor.lock().unwrap(); - update_alignment(&mut *editor, align); - } - Message::ThemeChanged(theme) => { - self.theme = match theme { - "Dark" => Theme::dark(), - "Light" => Theme::light(), - _ => return Command::none(), - }; - - let Color { r, g, b, a } = self.theme.cosmic().on_bg_color().into(); - let as_u8 = |component: f32| (component * 255.0) as u8; - self.attrs = self.attrs.color(cosmic_text::Color::rgba( - as_u8(r), - as_u8(g), - as_u8(b), - as_u8(a), - )); - - let mut editor = self.editor.lock().unwrap(); - - #[cfg(not(feature = "vi"))] - // Update the syntax color theme - match theme { - "Light" => editor.update_theme("base16-ocean.light"), - "Dark" | _ => editor.update_theme("base16-eighties.dark"), - }; - - update_attrs(&mut *editor, self.attrs); - } - } - - Command::none() - } - - fn view(&self) -> Element { - static THEMES: &[&str] = &["Dark", "Light"]; - let theme_picker = pick_list( - THEMES, - Some(match self.theme.theme_type { - ThemeType::Dark => THEMES[0], - ThemeType::Light => THEMES[1], - _ => unreachable!(), - }), - Message::ThemeChanged, - ); - - let font_size_picker = { - pick_list( - FontSize::all(), - Some(self.font_size), - Message::FontSizeChanged, - ) - }; - - let wrap_picker = { - let editor = self.editor.lock().unwrap(); - pick_list( - WRAP_MODE, - Some(editor.with_buffer(|buffer| buffer.wrap())), - Message::WrapChanged, - ) - }; - - let content: Element<_> = column![ - row![ - button(theme::Button::Secondary) - .text("Open") - .on_press(Message::Open), - button(theme::Button::Secondary) - .text("Save") - .on_press(Message::Save), - horizontal_space(Length::Fill), - text("Bold:"), - toggler( - None, - self.attrs.weight == cosmic_text::Weight::BOLD, - Message::Bold - ), - text("Italic:"), - toggler( - None, - self.attrs.style == cosmic_text::Style::Italic, - Message::Italic - ), - text("Monospaced:"), - toggler( - None, - self.attrs.family == cosmic_text::Family::Monospace, - Message::Monospaced - ), - text("Theme:"), - theme_picker, - text("Font Size:"), - font_size_picker, - ] - .align_items(Alignment::Center) - .spacing(8), - row![ - text("Wrap:"), - wrap_picker, - button(theme::Button::Text) - .icon(theme::Svg::Default, "format-justify-left", 20) - .on_press(Message::AlignmentChanged(Align::Left)), - button(theme::Button::Text) - .icon(theme::Svg::Symbolic, "format-justify-center", 20) - .on_press(Message::AlignmentChanged(Align::Center)), - button(theme::Button::Text) - .icon(theme::Svg::Symbolic, "format-justify-right", 20) - .on_press(Message::AlignmentChanged(Align::Right)), - button(theme::Button::Text) - .icon(theme::Svg::SymbolicLink, "format-justify-fill", 20) - .on_press(Message::AlignmentChanged(Align::Justified)), - ] - .align_items(Alignment::Center) - .spacing(8), - text_box(&self.editor) - ] - .spacing(8) - .padding(16) - .into(); - - // Uncomment to debug layout: content.explain(Color::WHITE) - content - } -} - -fn update_attrs<'buffer, E: Edit<'buffer>>(editor: &mut E, attrs: Attrs) { - editor.with_buffer_mut(|buffer| { - buffer.lines.iter_mut().for_each(|line| { - line.set_attrs_list(AttrsList::new(attrs)); - }); - }); -} - -fn update_alignment<'buffer, E: Edit<'buffer>>(editor: &mut E, align: Align) { - let current_line = editor.cursor().line; - let selection_bounds_opt = editor.selection_bounds(); - editor.with_buffer_mut(|buffer| { - if let Some((start, end)) = selection_bounds_opt { - if let Some(lines) = buffer.lines.get_mut(start.line..=end.line) { - for line in lines.iter_mut() { - line.set_align(Some(align)); - } - } - } else if let Some(line) = buffer.lines.get_mut(current_line) { - line.set_align(Some(align)); - } - }); -} diff --git a/examples/editor-libcosmic/src/text_box.rs b/examples/editor-libcosmic/src/text_box.rs deleted file mode 100644 index c6c0ff8..0000000 --- a/examples/editor-libcosmic/src/text_box.rs +++ /dev/null @@ -1,386 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use cosmic::{ - iced_core::{event::Status, widget::tree, *}, - iced_runtime::keyboard::KeyCode, - theme::{Theme, ThemeType}, -}; -use cosmic_text::{Action, Edit, Motion, SwashCache, ViEditor}; -use std::{cmp, sync::Mutex, time::Instant}; - -use crate::FONT_SYSTEM; - -pub struct Appearance { - background_color: Option, -} - -pub trait StyleSheet { - fn appearance(&self) -> Appearance; -} - -impl StyleSheet for Theme { - fn appearance(&self) -> Appearance { - match self.theme_type { - ThemeType::Dark | ThemeType::HighContrastDark | ThemeType::Custom(_) => Appearance { - background_color: Some(Color::from_rgb8(0x34, 0x34, 0x34)), - }, - ThemeType::Light | ThemeType::HighContrastLight => Appearance { - background_color: Some(Color::from_rgb8(0xFC, 0xFC, 0xFC)), - }, - } - } -} - -pub struct TextBox<'a, 'editor, 'buffer> { - editor: &'a Mutex>, - padding: Padding, -} - -impl<'a, 'editor, 'buffer> TextBox<'a, 'editor, 'buffer> { - pub fn new(editor: &'a Mutex>) -> Self { - Self { - editor, - padding: Padding::new(0.), - } - } - - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } -} - -pub fn text_box<'a, 'editor, 'buffer>( - editor: &'a Mutex>, -) -> TextBox<'a, 'editor, 'buffer> { - TextBox::new(editor) -} - -fn draw_pixel( - buffer: &mut [u8], - width: i32, - height: i32, - x: i32, - y: i32, - color: cosmic_text::Color, -) { - let alpha = (color.0 >> 24) & 0xFF; - if alpha == 0 { - // Do not draw if alpha is zero - return; - } - - if y < 0 || y >= height { - // Skip if y out of bounds - return; - } - - if x < 0 || x >= width { - // Skip if x out of bounds - return; - } - - let offset = (y as usize * width as usize + x as usize) * 4; - - let mut current = buffer[offset + 2] as u32 - | (buffer[offset + 1] as u32) << 8 - | (buffer[offset + 0] as u32) << 16 - | (buffer[offset + 3] as u32) << 24; - - if alpha >= 255 || current == 0 { - // Alpha is 100% or current is null, replace with no blending - current = color.0; - } else { - // Alpha blend with current value - let n_alpha = 255 - alpha; - let rb = ((n_alpha * (current & 0x00FF00FF)) + (alpha * (color.0 & 0x00FF00FF))) >> 8; - let ag = (n_alpha * ((current & 0xFF00FF00) >> 8)) - + (alpha * (0x01000000 | ((color.0 & 0x0000FF00) >> 8))); - current = (rb & 0x00FF00FF) | (ag & 0xFF00FF00); - } - - buffer[offset + 2] = current as u8; - buffer[offset + 1] = (current >> 8) as u8; - buffer[offset + 0] = (current >> 16) as u8; - buffer[offset + 3] = (current >> 24) as u8; -} - -impl<'a, 'editor, 'buffer, Message, Renderer> Widget - for TextBox<'a, 'editor, 'buffer> -where - Renderer: cosmic::iced_core::Renderer + image::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill - } - - fn layout(&self, _renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - let limits = limits.width(Length::Fill).height(Length::Fill); - - let mut editor = self.editor.lock().unwrap(); - editor - .borrow_with(&mut FONT_SYSTEM.lock().unwrap()) - .shape_as_needed(true); - - let mut layout_lines = 0; - editor.with_buffer(|buffer| { - for line in buffer.lines.iter() { - match line.layout_opt() { - Some(layout) => layout_lines += layout.len(), - None => (), - } - } - }); - - let height = - layout_lines as f32 * editor.with_buffer(|buffer| buffer.metrics().line_height); - let size = Size::new(limits.max().width, height); - - layout::Node::new(limits.resolve(size)) - } - - fn mouse_interaction( - &self, - _tree: &widget::Tree, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - if cursor_position.is_over(layout.bounds()) { - mouse::Interaction::Text - } else { - mouse::Interaction::Idle - } - } - - fn draw( - &self, - tree: &widget::Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: mouse::Cursor, - viewport: &Rectangle, - ) { - let instant = Instant::now(); - - let state = tree.state.downcast_ref::(); - - let appearance = theme.appearance(); - - if let Some(background_color) = appearance.background_color { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - background_color, - ); - } - - let mut editor = self.editor.lock().unwrap(); - - let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) - - self.padding.horizontal() as i32; - let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32) - - self.padding.vertical() as i32; - - const SCALE_FACTOR: f64 = 1.; - - let image_w = (view_w as f64 * SCALE_FACTOR) as i32; - let image_h = (view_h as f64 * SCALE_FACTOR) as i32; - - let mut font_system = FONT_SYSTEM.lock().unwrap(); - let mut editor = editor.borrow_with(&mut font_system); - - // Scale metrics - let metrics = editor.with_buffer(|buffer| buffer.metrics()); - editor.with_buffer_mut(|buffer| buffer.set_metrics(metrics.scale(SCALE_FACTOR as f32))); - - // Set size - editor.with_buffer_mut(|buffer| buffer.set_size(image_w as f32, image_h as f32)); - - // Shape and layout - editor.shape_as_needed(true); - - // Draw to pixel buffer - let mut pixels = vec![0; image_w as usize * image_h as usize * 4]; - editor.draw(&mut state.cache.lock().unwrap(), |x, y, w, h, color| { - //TODO: improve performance - for row in 0..h as i32 { - for col in 0..w as i32 { - draw_pixel(&mut pixels, image_w, image_h, x + col, y + row, color); - } - } - }); - - // Restore original metrics - editor.with_buffer_mut(|buffer| buffer.set_metrics(metrics)); - - let handle = image::Handle::from_pixels(image_w as u32, image_h as u32, pixels); - image::Renderer::draw( - renderer, - handle, - Rectangle::new( - layout.position() + [self.padding.left as f32, self.padding.top as f32].into(), - Size::new(view_w as f32, view_h as f32), - ), - ); - - let duration = instant.elapsed(); - log::debug!("redraw {}, {}: {:?}", view_w, view_h, duration); - } - - fn on_event( - &mut self, - tree: &mut widget::Tree, - event: Event, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> Status { - let state = tree.state.downcast_mut::(); - let mut editor = self.editor.lock().unwrap(); - let mut font_system = FONT_SYSTEM.lock().unwrap(); - let mut editor = editor.borrow_with(&mut font_system); - - let mut status = Status::Ignored; - match event { - Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => match key_code { - KeyCode::Left => { - editor.action(Action::Motion(Motion::Left)); - status = Status::Captured; - } - KeyCode::Right => { - editor.action(Action::Motion(Motion::Right)); - status = Status::Captured; - } - KeyCode::Up => { - editor.action(Action::Motion(Motion::Up)); - status = Status::Captured; - } - KeyCode::Down => { - editor.action(Action::Motion(Motion::Down)); - status = Status::Captured; - } - KeyCode::Home => { - editor.action(Action::Motion(Motion::Home)); - status = Status::Captured; - } - KeyCode::End => { - editor.action(Action::Motion(Motion::End)); - status = Status::Captured; - } - KeyCode::PageUp => { - editor.action(Action::Motion(Motion::PageUp)); - status = Status::Captured; - } - KeyCode::PageDown => { - editor.action(Action::Motion(Motion::PageDown)); - status = Status::Captured; - } - KeyCode::Escape => { - editor.action(Action::Escape); - status = Status::Captured; - } - KeyCode::Enter => { - editor.action(Action::Enter); - status = Status::Captured; - } - KeyCode::Backspace => { - editor.action(Action::Backspace); - status = Status::Captured; - } - KeyCode::Delete => { - editor.action(Action::Delete); - status = Status::Captured; - } - _ => (), - }, - Event::Keyboard(keyboard::Event::CharacterReceived(character)) => { - editor.action(Action::Insert(character)); - status = Status::Captured; - } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if let Some(position_in) = cursor_position.position_in(layout.bounds()) { - editor.action(Action::Click { - x: position_in.x as i32 - self.padding.left as i32, - y: position_in.y as i32 - self.padding.top as i32, - }); - state.is_dragging = true; - status = Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - state.is_dragging = false; - status = Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { position }) => { - if state.is_dragging { - editor.action(Action::Drag { - x: (position.x - layout.bounds().x) as i32 - self.padding.left as i32, - y: (position.y - layout.bounds().y) as i32 - self.padding.top as i32, - }); - status = Status::Captured; - } - } - Event::Mouse(mouse::Event::WheelScrolled { delta }) => match delta { - mouse::ScrollDelta::Lines { y, .. } => { - editor.action(Action::Scroll { - lines: (-y * 6.0) as i32, - }); - status = Status::Captured; - } - _ => (), - }, - _ => (), - } - - status - } -} - -impl<'a, 'editor, 'buffer, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Renderer: renderer::Renderer + image::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from(text_box: TextBox<'a, 'editor, 'buffer>) -> Self { - Self::new(text_box) - } -} - -pub struct State { - is_dragging: bool, - cache: Mutex, -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State { - is_dragging: false, - cache: Mutex::new(SwashCache::new()), - } - } -}