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;
+ }
+}