Add find stub

This commit is contained in:
Jeremy Soller 2024-01-11 13:20:16 -07:00
parent 8766819ea8
commit 928269a239
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
9 changed files with 266 additions and 17 deletions

View file

@ -20,6 +20,11 @@ use-bright-bold = Use bright colors with bold text
default-font-size = Default font size default-font-size = Default font size
default-zoom-step = Default zoom step default-zoom-step = Default zoom step
# Find
find-placeholder = Find...
find-previous = Find previous
find-next = Find next
# Menu # Menu
## File ## File

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 2.00305L0 8.00305L5 14.0031H16V2.00305H5ZM7 5.00305H8C8.277 5.00305 8.526 5.11505 8.707 5.29605L10 6.58905L11.293 5.29605C11.3856 5.20288 11.4958 5.129 11.6171 5.0787C11.7385 5.02841 11.8686 5.0027 12 5.00305H13V6.00305C13.0002 6.1344 12.9744 6.26448 12.9241 6.38582C12.8738 6.50715 12.8 6.61735 12.707 6.71005L11.414 8.00305L12.707 9.29605C12.887 9.47605 13 9.72605 13 10.0031V11.0031H12C11.8687 11.0032 11.7386 10.9775 11.6172 10.9272C11.4959 10.8769 11.3857 10.8031 11.293 10.7101L10 9.41705L8.707 10.7101C8.6143 10.8031 8.50409 10.8769 8.38275 10.9272C8.26141 10.9775 8.13134 11.0032 8 11.0031H7V10.0031C7 9.72605 7.112 9.47605 7.293 9.29605L8.586 8.00305L7.293 6.71005C7.19996 6.61735 7.12618 6.50715 7.0759 6.38582C7.02561 6.26448 6.99981 6.1344 7 6.00305V5.00305Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 904 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.44039 6.02242L7.50339 12.0844L13.5654 6.02242C15.0034 4.58442 13.5034 3.33442 12.4404 4.39742L7.50339 9.33442L2.56539 4.39742C1.75339 3.58442 0.253389 4.83442 1.44039 6.02242Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.44039 10.063L7.50339 4L13.5654 10.063C15.0034 11.5 13.5034 12.75 12.4404 11.688L7.50339 6.75L2.56539 11.688C1.75339 12.5 0.253389 11.25 1.44039 10.063Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4.00299C4.73478 4.00299 4.48043 4.10835 4.29289 4.29588C4.10536 4.48342 4 4.73777 4 5.00299C4.00006 5.26819 4.10545 5.5225 4.293 5.70999L6.586 8.00299L4.293 10.296C4.10545 10.4835 4.00006 10.7378 4 11.003C4 11.2682 4.10536 11.5226 4.29289 11.7101C4.48043 11.8976 4.73478 12.003 5 12.003C5.26519 12.0029 5.51951 11.8975 5.707 11.71L8 9.41699L10.283 11.7C10.3762 11.7959 10.4877 11.8721 10.6108 11.9241C10.734 11.9762 10.8663 12.003 11 12.003C11.2652 12.003 11.5196 11.8976 11.7071 11.7101C11.8946 11.5226 12 11.2682 12 11.003C11.9999 10.7378 11.8945 10.4835 11.707 10.296L9.414 8.00299L11.697 5.71999C11.7929 5.6268 11.8691 5.51534 11.9211 5.39218C11.9732 5.26903 12 5.13669 12 5.00299C12 4.73777 11.8946 4.48342 11.7071 4.29588C11.5196 4.10835 11.2652 4.00299 11 4.00299C10.7348 4.00305 10.4805 4.10844 10.293 4.29599L8 6.58899L5.717 4.30599C5.71369 4.30263 5.71036 4.2993 5.707 4.29599C5.51951 4.10844 5.26519 4.00305 5 4.00299Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

49
src/icon_cache.rs Normal file
View file

@ -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<IconCacheKey, icon::Handle>,
}
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)
}
}

View file

@ -14,10 +14,10 @@ use cosmic::{
futures::SinkExt, futures::SinkExt,
keyboard::{Event as KeyEvent, KeyCode, Modifiers}, keyboard::{Event as KeyEvent, KeyCode, Modifiers},
subscription::{self, Subscription}, subscription::{self, Subscription},
window, Event, Length, Padding, Point, window, Alignment, Event, Length, Padding, Point,
}, },
style, style,
widget::{self, segmented_button}, widget::{self, button, segmented_button},
Application, ApplicationExt, Element, Application, ApplicationExt, Element,
}; };
use cosmic_text::{fontdb::FaceInfo, Family, Stretch, Weight}; use cosmic_text::{fontdb::FaceInfo, Family, Stretch, Weight};
@ -33,6 +33,9 @@ use tokio::sync::mpsc;
use config::{AppTheme, Config, CONFIG_VERSION}; use config::{AppTheme, Config, CONFIG_VERSION};
mod config; mod config;
use icon_cache::IconCache;
mod icon_cache;
mod localize; mod localize;
use menu::menu_bar; use menu::menu_bar;
@ -46,6 +49,15 @@ mod terminal_box;
mod terminal_theme; mod terminal_theme;
lazy_static::lazy_static! {
static ref ICON_CACHE: Mutex<IconCache> = 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 /// Runs application with these settings
#[rustfmt::skip] #[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -150,6 +162,11 @@ pub enum Message {
DefaultFontWeight(usize), DefaultFontWeight(usize),
DefaultBoldFontWeight(usize), DefaultBoldFontWeight(usize),
DefaultZoomStep(usize), DefaultZoomStep(usize),
Find(bool),
FindNext,
FindPrevious,
FindSearchValueChanged(String),
Modifiers(Modifiers),
Paste(Option<segmented_button::Entity>), Paste(Option<segmented_button::Entity>),
PasteValue(Option<segmented_button::Entity>, String), PasteValue(Option<segmented_button::Entity>, String),
SelectAll(Option<segmented_button::Entity>), SelectAll(Option<segmented_button::Entity>),
@ -210,9 +227,14 @@ pub struct App {
theme_names: Vec<String>, theme_names: Vec<String>,
themes: HashMap<String, TermColors>, themes: HashMap<String, TermColors>,
context_page: ContextPage, context_page: ContextPage,
terminal_id: widget::Id,
find: bool,
find_search_id: widget::Id,
find_search_value: String,
term_event_tx_opt: Option<mpsc::Sender<(segmented_button::Entity, TermEvent)>>, term_event_tx_opt: Option<mpsc::Sender<(segmented_button::Entity, TermEvent)>>,
term_config: TermConfig, term_config: TermConfig,
show_advanced_font_settings: bool, show_advanced_font_settings: bool,
modifiers: Modifiers,
} }
impl App { impl App {
@ -243,6 +265,17 @@ impl App {
self.update_config() self.update_config()
} }
fn update_focus(&self) -> Command<Message> {
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<Message> { fn update_title(&mut self) -> Command<Message> {
let (header_title, window_title) = match self.tab_model.text(self.tab_model.active()) { let (header_title, window_title) = match self.tab_model.text(self.tab_model.active()) {
Some(tab_title) => ( Some(tab_title) => (
@ -252,7 +285,7 @@ impl App {
None => (String::new(), "COSMIC Terminal".to_string()), None => (String::new(), "COSMIC Terminal".to_string()),
}; };
self.set_header_title(header_title); 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) { fn set_curr_font_weights_and_stretches(&mut self) {
@ -631,9 +664,14 @@ impl Application for App {
theme_names, theme_names,
themes, themes,
context_page: ContextPage::Settings, 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_config: flags.term_config,
term_event_tx_opt: None, term_event_tx_opt: None,
show_advanced_font_settings: false, show_advanced_font_settings: false,
modifiers: Modifiers::empty(),
}; };
app.set_curr_font_weights_and_stretches(); app.set_curr_font_weights_and_stretches();
@ -642,6 +680,20 @@ impl Application for App {
(app, command) (app, command)
} }
//TODO: currently the first escape unfocuses, and the second calls this function
fn on_escape(&mut self) -> Command<Message> {
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. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
@ -746,6 +798,34 @@ impl Application for App {
log::warn!("failed to find zoom step with index {}", index); 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) => { Message::Paste(entity_opt) => {
return clipboard::read(move |value_opt| match value_opt { return clipboard::read(move |value_opt| match value_opt {
Some(value) => message::app(Message::PasteValue(entity_opt, value)), Some(value) => message::app(Message::PasteValue(entity_opt, value)),
@ -1009,9 +1089,11 @@ impl Application for App {
let entity = self.tab_model.active(); let entity = self.tab_model.active();
match self.tab_model.data::<Mutex<Terminal>>(entity) { match self.tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => { Some(terminal) => {
let terminal_box = terminal_box(terminal).on_context_menu(move |position_opt| { let terminal_box = terminal_box(terminal)
Message::TabContextMenu(entity, position_opt) .id(self.terminal_id.clone())
}); .on_context_menu(move |position_opt| {
Message::TabContextMenu(entity, position_opt)
});
let context_menu = { let context_menu = {
let terminal = terminal.lock().unwrap(); 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(); let content: Element<_> = tab_column.into();
// Uncomment to debug layout: // Uncomment to debug layout:
@ -1068,6 +1204,16 @@ impl Application for App {
None 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 { Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::T, key_code: KeyCode::T,
modifiers, modifiers,
@ -1118,6 +1264,9 @@ impl Application for App {
None None
} }
} }
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
Some(Message::Modifiers(modifiers))
}
_ => None, _ => None,
}), }),
subscription::channel( subscription::channel(

View file

@ -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![ MenuBar::new(vec![
MenuTree::with_children( MenuTree::with_children(
menu_root(fl!("file")), 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!("paste"), Message::Paste(None)),
menu_item(fl!("select-all"), Message::SelectAll(None)), menu_item(fl!("select-all"), Message::SelectAll(None)),
MenuTree::new(horizontal_rule(1)), MenuTree::new(horizontal_rule(1)),
menu_key(fl!("find"), "Ctrl + F", Message::Todo("find")), menu_item(fl!("find"), Message::Find(true)),
], ],
), ),
MenuTree::with_children( MenuTree::with_children(

View file

@ -19,7 +19,11 @@ use cosmic::{
layout::{self, Layout}, layout::{self, Layout},
renderer::{self, Quad, Renderer as _}, renderer::{self, Quad, Renderer as _},
text::Renderer as _, text::Renderer as _,
widget::{self, tree, Widget}, widget::{
self,
operation::{self, Operation, OperationOutputWrapper},
tree, Id, Widget,
},
Shell, Shell,
}, },
theme::Theme, theme::Theme,
@ -37,6 +41,7 @@ use crate::{Terminal, TerminalScroll};
pub struct TerminalBox<'a, Message> { pub struct TerminalBox<'a, Message> {
terminal: &'a Mutex<Terminal>, terminal: &'a Mutex<Terminal>,
id: Option<Id>,
padding: Padding, padding: Padding,
click_timing: Duration, click_timing: Duration,
context_menu: Option<Point>, context_menu: Option<Point>,
@ -50,6 +55,7 @@ where
pub fn new(terminal: &'a Mutex<Terminal>) -> Self { pub fn new(terminal: &'a Mutex<Terminal>) -> Self {
Self { Self {
terminal, terminal,
id: None,
padding: Padding::new(0.0), padding: Padding::new(0.0),
click_timing: Duration::from_millis(500), click_timing: Duration::from_millis(500),
context_menu: None, context_menu: None,
@ -57,6 +63,11 @@ where
} }
} }
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into(); self.padding = padding.into();
self self
@ -149,6 +160,18 @@ where
}) })
} }
fn operate(
&self,
tree: &mut widget::Tree,
_layout: Layout<'_>,
_renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
) {
let state = tree.state.downcast_mut::<State>();
operation.focusable(state, self.id.as_ref());
}
fn mouse_interaction( fn mouse_interaction(
&self, &self,
tree: &widget::Tree, tree: &widget::Tree,
@ -427,7 +450,7 @@ where
Event::Keyboard(KeyEvent::KeyPressed { Event::Keyboard(KeyEvent::KeyPressed {
key_code, key_code,
modifiers, modifiers,
}) => match ( }) if state.is_focused => match (
modifiers.logo(), modifiers.logo(),
modifiers.control(), modifiers.control(),
modifiers.alt(), modifiers.alt(),
@ -670,7 +693,7 @@ where
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
state.modifiers = modifiers; state.modifiers = modifiers;
} }
Event::Keyboard(KeyEvent::CharacterReceived(character)) => { Event::Keyboard(KeyEvent::CharacterReceived(character)) if state.is_focused => {
match ( match (
state.modifiers.logo(), state.modifiers.logo(),
state.modifiers.control(), state.modifiers.control(),
@ -717,6 +740,8 @@ where
} }
Event::Mouse(MouseEvent::ButtonPressed(button)) => { Event::Mouse(MouseEvent::ButtonPressed(button)) => {
if let Some(p) = cursor_position.position_in(layout.bounds()) { if let Some(p) = cursor_position.position_in(layout.bounds()) {
state.is_focused = true;
// Handle left click drag // Handle left click drag
if let Button::Left = button { if let Button::Left = button {
let x = p.x - self.padding.left; let x = p.x - self.padding.left;
@ -915,6 +940,7 @@ pub struct State {
modifiers: Modifiers, modifiers: Modifiers,
click: Option<(ClickKind, Instant)>, click: Option<(ClickKind, Instant)>,
dragging: Option<Dragging>, dragging: Option<Dragging>,
is_focused: bool,
scroll_pixels: f32, scroll_pixels: f32,
scrollbar_rect: Cell<Rectangle<f32>>, scrollbar_rect: Cell<Rectangle<f32>>,
} }
@ -926,8 +952,23 @@ impl State {
modifiers: Modifiers::empty(), modifiers: Modifiers::empty(),
click: None, click: None,
dragging: None, dragging: None,
is_focused: false,
scroll_pixels: 0.0, scroll_pixels: 0.0,
scrollbar_rect: Cell::new(Rectangle::default()), 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;
}
}