From 928269a2394b6b6a74b2fcca89b4ef17ed672020 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 11 Jan 2024 13:20:16 -0700 Subject: [PATCH] Add find stub --- i18n/en/cosmic_term.ftl | 5 + res/icons/edit-clear-symbolic.svg | 3 + res/icons/go-down-symbolic.svg | 3 + res/icons/go-up-symbolic.svg | 3 + res/icons/window-close-symbolic.svg | 3 + src/icon_cache.rs | 49 +++++++++ src/main.rs | 161 ++++++++++++++++++++++++++-- src/menu.rs | 9 +- src/terminal_box.rs | 47 +++++++- 9 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 res/icons/edit-clear-symbolic.svg create mode 100644 res/icons/go-down-symbolic.svg create mode 100644 res/icons/go-up-symbolic.svg create mode 100644 res/icons/window-close-symbolic.svg create mode 100644 src/icon_cache.rs diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 11e9031..6ca2614 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -20,6 +20,11 @@ use-bright-bold = Use bright colors with bold text default-font-size = Default font size default-zoom-step = Default zoom step +# Find +find-placeholder = Find... +find-previous = Find previous +find-next = Find next + # Menu ## File diff --git a/res/icons/edit-clear-symbolic.svg b/res/icons/edit-clear-symbolic.svg new file mode 100644 index 0000000..bcf0a70 --- /dev/null +++ b/res/icons/edit-clear-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/icons/go-down-symbolic.svg b/res/icons/go-down-symbolic.svg new file mode 100644 index 0000000..5e89982 --- /dev/null +++ b/res/icons/go-down-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/icons/go-up-symbolic.svg b/res/icons/go-up-symbolic.svg new file mode 100644 index 0000000..3747d43 --- /dev/null +++ b/res/icons/go-up-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/icons/window-close-symbolic.svg b/res/icons/window-close-symbolic.svg new file mode 100644 index 0000000..2533639 --- /dev/null +++ b/res/icons/window-close-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icon_cache.rs b/src/icon_cache.rs new file mode 100644 index 0000000..24d5024 --- /dev/null +++ b/src/icon_cache.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::widget::icon; +use std::collections::HashMap; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct IconCacheKey { + name: &'static str, + size: u16, +} + +pub struct IconCache { + cache: HashMap, +} + +impl IconCache { + pub fn new() -> Self { + let mut cache = HashMap::new(); + + macro_rules! bundle { + ($name:expr, $size:expr) => { + let data: &'static [u8] = include_bytes!(concat!("../res/icons/", $name, ".svg")); + cache.insert( + IconCacheKey { + name: $name, + size: $size, + }, + icon::from_svg_bytes(data).symbolic(true), + ); + }; + } + + bundle!("edit-clear-symbolic", 16); + bundle!("go-down-symbolic", 16); + bundle!("go-up-symbolic", 16); + bundle!("window-close-symbolic", 16); + + Self { cache } + } + + pub fn get(&mut self, name: &'static str, size: u16) -> icon::Icon { + let handle = self + .cache + .entry(IconCacheKey { name, size }) + .or_insert_with(|| icon::from_name(name).size(size).handle()) + .clone(); + icon::icon(handle).size(size) + } +} diff --git a/src/main.rs b/src/main.rs index 90e4282..296bae5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,10 +14,10 @@ use cosmic::{ futures::SinkExt, keyboard::{Event as KeyEvent, KeyCode, Modifiers}, subscription::{self, Subscription}, - window, Event, Length, Padding, Point, + window, Alignment, Event, Length, Padding, Point, }, style, - widget::{self, segmented_button}, + widget::{self, button, segmented_button}, Application, ApplicationExt, Element, }; use cosmic_text::{fontdb::FaceInfo, Family, Stretch, Weight}; @@ -33,6 +33,9 @@ use tokio::sync::mpsc; use config::{AppTheme, Config, CONFIG_VERSION}; mod config; +use icon_cache::IconCache; +mod icon_cache; + mod localize; use menu::menu_bar; @@ -46,6 +49,15 @@ mod terminal_box; mod terminal_theme; +lazy_static::lazy_static! { + static ref ICON_CACHE: Mutex = Mutex::new(IconCache::new()); +} + +pub fn icon_cache_get(name: &'static str, size: u16) -> widget::icon::Icon { + let mut icon_cache = ICON_CACHE.lock().unwrap(); + icon_cache.get(name, size) +} + /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { @@ -150,6 +162,11 @@ pub enum Message { DefaultFontWeight(usize), DefaultBoldFontWeight(usize), DefaultZoomStep(usize), + Find(bool), + FindNext, + FindPrevious, + FindSearchValueChanged(String), + Modifiers(Modifiers), Paste(Option), PasteValue(Option, String), SelectAll(Option), @@ -210,9 +227,14 @@ pub struct App { theme_names: Vec, themes: HashMap, context_page: ContextPage, + terminal_id: widget::Id, + find: bool, + find_search_id: widget::Id, + find_search_value: String, term_event_tx_opt: Option>, term_config: TermConfig, show_advanced_font_settings: bool, + modifiers: Modifiers, } impl App { @@ -243,6 +265,17 @@ impl App { self.update_config() } + fn update_focus(&self) -> Command { + if self.core.window.show_context { + Command::none() + } else if self.find { + widget::text_input::focus(self.find_search_id.clone()) + } else { + widget::text_input::focus(self.terminal_id.clone()) + } + } + + // Call this any time the tab changes fn update_title(&mut self) -> Command { let (header_title, window_title) = match self.tab_model.text(self.tab_model.active()) { Some(tab_title) => ( @@ -252,7 +285,7 @@ impl App { None => (String::new(), "COSMIC Terminal".to_string()), }; self.set_header_title(header_title); - self.set_window_title(window_title) + Command::batch([self.set_window_title(window_title), self.update_focus()]) } fn set_curr_font_weights_and_stretches(&mut self) { @@ -631,9 +664,14 @@ impl Application for App { theme_names, themes, context_page: ContextPage::Settings, + terminal_id: widget::Id::unique(), + find: false, + find_search_id: widget::Id::unique(), + find_search_value: String::new(), term_config: flags.term_config, term_event_tx_opt: None, show_advanced_font_settings: false, + modifiers: Modifiers::empty(), }; app.set_curr_font_weights_and_stretches(); @@ -642,6 +680,20 @@ impl Application for App { (app, command) } + //TODO: currently the first escape unfocuses, and the second calls this function + fn on_escape(&mut self) -> Command { + if self.core.window.show_context { + // Close context drawer if open + self.core.window.show_context = false; + } else if self.find { + // Close find if open + self.find = false; + } + + // Focus correct widget + self.update_focus() + } + /// Handle application events here. fn update(&mut self, message: Self::Message) -> Command { match message { @@ -746,6 +798,34 @@ impl Application for App { log::warn!("failed to find zoom step with index {}", index); } }, + Message::Find(find) => { + self.find = find; + + // Focus correct input + return self.update_focus(); + } + Message::FindNext => { + if !self.find_search_value.is_empty() { + //TODO + } + + // Focus correct input + return self.update_focus(); + } + Message::FindPrevious => { + if !self.find_search_value.is_empty() { + //TODO + } + + // Focus correct input + return self.update_focus(); + } + Message::FindSearchValueChanged(value) => { + self.find_search_value = value; + } + Message::Modifiers(modifiers) => { + self.modifiers = modifiers; + } Message::Paste(entity_opt) => { return clipboard::read(move |value_opt| match value_opt { Some(value) => message::app(Message::PasteValue(entity_opt, value)), @@ -1009,9 +1089,11 @@ impl Application for App { let entity = self.tab_model.active(); match self.tab_model.data::>(entity) { Some(terminal) => { - let terminal_box = terminal_box(terminal).on_context_menu(move |position_opt| { - Message::TabContextMenu(entity, position_opt) - }); + let terminal_box = terminal_box(terminal) + .id(self.terminal_id.clone()) + .on_context_menu(move |position_opt| { + Message::TabContextMenu(entity, position_opt) + }); let context_menu = { let terminal = terminal.lock().unwrap(); @@ -1034,6 +1116,60 @@ impl Application for App { } } + if self.find { + let find_input = + widget::text_input::text_input(fl!("find-placeholder"), &self.find_search_value) + .id(self.find_search_id.clone()) + .on_input(Message::FindSearchValueChanged) + .on_submit(if self.modifiers.contains(Modifiers::SHIFT) { + Message::FindPrevious + } else { + Message::FindNext + }) + .width(Length::Fixed(320.0)) + .trailing_icon( + button(icon_cache_get("edit-clear-symbolic", 16)) + .on_press(Message::FindSearchValueChanged(String::new())) + .style(style::Button::Icon) + .into(), + ); + let find_widget = widget::row::with_children(vec![ + find_input.into(), + widget::tooltip( + button(icon_cache_get("go-up-symbolic", 16)) + .on_press(Message::FindPrevious) + .padding(space_xxs) + .style(style::Button::Icon), + fl!("find-previous"), + widget::tooltip::Position::Top, + ) + .into(), + widget::tooltip( + button(icon_cache_get("go-down-symbolic", 16)) + .on_press(Message::FindNext) + .padding(space_xxs) + .style(style::Button::Icon), + fl!("find-next"), + widget::tooltip::Position::Top, + ) + .into(), + widget::horizontal_space(Length::Fill).into(), + button(icon_cache_get("window-close-symbolic", 16)) + .on_press(Message::Find(false)) + .padding(space_xxs) + .style(style::Button::Icon) + .into(), + ]) + .align_items(Alignment::Center) + .padding(space_xxs) + .spacing(space_xxs); + + tab_column = tab_column.push( + widget::cosmic_container::container(find_widget) + .layer(cosmic_theme::Layer::Primary), + ); + } + let content: Element<_> = tab_column.into(); // Uncomment to debug layout: @@ -1068,6 +1204,16 @@ impl Application for App { None } } + Event::Keyboard(KeyEvent::KeyPressed { + key_code: KeyCode::F, + modifiers, + }) => { + if modifiers == Modifiers::CTRL | Modifiers::SHIFT { + Some(Message::Find(true)) + } else { + None + } + } Event::Keyboard(KeyEvent::KeyPressed { key_code: KeyCode::T, modifiers, @@ -1118,6 +1264,9 @@ impl Application for App { None } } + Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { + Some(Message::Modifiers(modifiers)) + } _ => None, }), subscription::channel( diff --git a/src/menu.rs b/src/menu.rs index 64d7041..cdf3e1b 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -105,13 +105,6 @@ pub fn menu_bar<'a>() -> Element<'a, Message> { ) }; - let menu_key = |label, key, message| { - MenuTree::new( - menu_button!(widget::text(label), horizontal_space(Length::Fill), key) - .on_press(message), - ) - }; - MenuBar::new(vec![ MenuTree::with_children( menu_root(fl!("file")), @@ -131,7 +124,7 @@ pub fn menu_bar<'a>() -> Element<'a, Message> { menu_item(fl!("paste"), Message::Paste(None)), menu_item(fl!("select-all"), Message::SelectAll(None)), MenuTree::new(horizontal_rule(1)), - menu_key(fl!("find"), "Ctrl + F", Message::Todo("find")), + menu_item(fl!("find"), Message::Find(true)), ], ), MenuTree::with_children( diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 8da7555..3029528 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -19,7 +19,11 @@ use cosmic::{ layout::{self, Layout}, renderer::{self, Quad, Renderer as _}, text::Renderer as _, - widget::{self, tree, Widget}, + widget::{ + self, + operation::{self, Operation, OperationOutputWrapper}, + tree, Id, Widget, + }, Shell, }, theme::Theme, @@ -37,6 +41,7 @@ use crate::{Terminal, TerminalScroll}; pub struct TerminalBox<'a, Message> { terminal: &'a Mutex, + id: Option, padding: Padding, click_timing: Duration, context_menu: Option, @@ -50,6 +55,7 @@ where pub fn new(terminal: &'a Mutex) -> Self { Self { terminal, + id: None, padding: Padding::new(0.0), click_timing: Duration::from_millis(500), context_menu: None, @@ -57,6 +63,11 @@ where } } + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); self @@ -149,6 +160,18 @@ where }) } + fn operate( + &self, + tree: &mut widget::Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + let state = tree.state.downcast_mut::(); + + operation.focusable(state, self.id.as_ref()); + } + fn mouse_interaction( &self, tree: &widget::Tree, @@ -427,7 +450,7 @@ where Event::Keyboard(KeyEvent::KeyPressed { key_code, modifiers, - }) => match ( + }) if state.is_focused => match ( modifiers.logo(), modifiers.control(), modifiers.alt(), @@ -670,7 +693,7 @@ where Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { state.modifiers = modifiers; } - Event::Keyboard(KeyEvent::CharacterReceived(character)) => { + Event::Keyboard(KeyEvent::CharacterReceived(character)) if state.is_focused => { match ( state.modifiers.logo(), state.modifiers.control(), @@ -717,6 +740,8 @@ where } Event::Mouse(MouseEvent::ButtonPressed(button)) => { if let Some(p) = cursor_position.position_in(layout.bounds()) { + state.is_focused = true; + // Handle left click drag if let Button::Left = button { let x = p.x - self.padding.left; @@ -915,6 +940,7 @@ pub struct State { modifiers: Modifiers, click: Option<(ClickKind, Instant)>, dragging: Option, + is_focused: bool, scroll_pixels: f32, scrollbar_rect: Cell>, } @@ -926,8 +952,23 @@ impl State { modifiers: Modifiers::empty(), click: None, dragging: None, + is_focused: false, scroll_pixels: 0.0, scrollbar_rect: Cell::new(Rectangle::default()), } } } + +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + self.is_focused + } + + fn focus(&mut self) { + self.is_focused = true; + } + + fn unfocus(&mut self) { + self.is_focused = false; + } +}