Implement find and improvements for focus

This commit is contained in:
Jeremy Soller 2024-01-09 12:40:29 -07:00
parent 81a0bbc094
commit f3a8fef8f8
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
10 changed files with 358 additions and 105 deletions

View file

@ -19,6 +19,8 @@ pub enum Action {
CloseProject,
Copy,
Cut,
Find,
FindAndReplace,
NewFile,
NewWindow,
OpenFileDialog,
@ -42,6 +44,8 @@ impl Action {
Self::CloseProject => Message::CloseProject,
Self::Copy => Message::Copy,
Self::Cut => Message::Cut,
Self::Find => Message::Find(Some(false)),
Self::FindAndReplace => Message::Find(Some(true)),
Self::NewFile => Message::NewFile,
Self::NewWindow => Message::NewWindow,
Self::OpenFileDialog => Message::OpenFileDialog,
@ -111,6 +115,8 @@ impl KeyBind {
bind!([Ctrl], W, CloseFile);
bind!([Ctrl], X, Cut);
bind!([Ctrl], C, Copy);
bind!([Ctrl], F, Find);
bind!([Ctrl], H, FindAndReplace);
bind!([Ctrl], V, Paste);
bind!([Ctrl], T, NewFile);
bind!([Ctrl], N, NewWindow);

View file

@ -30,9 +30,11 @@ impl IconCache {
};
}
bundle!("edit-clear-symbolic", 16);
bundle!("folder-open-symbolic", 16);
bundle!("go-down-symbolic", 16);
bundle!("go-next-symbolic", 16);
bundle!("go-up-symbolic", 16);
bundle!("list-add-symbolic", 16);
bundle!("object-select-symbolic", 16);
bundle!("window-close-symbolic", 16);

View file

@ -181,6 +181,11 @@ pub enum Message {
Cut,
DefaultFont(usize),
DefaultFontSize(usize),
Find(Option<bool>),
FindNext,
FindPrevious,
FindReplaceValueChanged(String),
FindSearchValueChanged(String),
GitProjectStatus(Vec<(String, PathBuf, Vec<GitStatus>)>),
Key(keyboard::Modifiers, keyboard::KeyCode),
NewFile,
@ -241,6 +246,13 @@ impl ContextPage {
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Find {
None,
Find,
FindAndReplace,
}
pub struct App {
core: Core,
nav_model: segmented_button::SingleSelectModel,
@ -253,6 +265,11 @@ pub struct App {
font_sizes: Vec<u16>,
theme_names: Vec<String>,
context_page: ContextPage,
text_box_id: widget::Id,
find_opt: Option<bool>,
find_replace_value: String,
find_search_id: widget::Id,
find_search_value: String,
git_project_status: Option<Vec<(String, PathBuf, Vec<GitStatus>)>>,
projects: Vec<(String, PathBuf)>,
project_search_id: widget::Id,
@ -440,6 +457,21 @@ impl App {
self.update_config()
}
fn update_focus(&self) -> Command<Message> {
if self.core.window.show_context {
match self.context_page {
ContextPage::ProjectSearch => {
widget::text_input::focus(self.project_search_id.clone())
}
_ => Command::none(),
}
} else if self.find_opt.is_some() {
widget::text_input::focus(self.find_search_id.clone())
} else {
widget::text_input::focus(self.text_box_id.clone())
}
}
fn update_nav_bar_active(&mut self) {
let tab_path_opt = match self.active_tab() {
Some(Tab::Editor(tab)) => tab.path_opt.clone(),
@ -504,7 +536,7 @@ impl App {
let window_title = format!("{title} - COSMIC Text Editor");
self.set_header_title(title.clone());
self.set_window_title(window_title)
Command::batch([self.set_window_title(window_title), self.update_focus()])
}
fn document_statistics(&self) -> Element<Message> {
@ -942,6 +974,11 @@ impl Application for App {
font_sizes,
theme_names,
context_page: ContextPage::Settings,
text_box_id: widget::Id::unique(),
find_opt: None,
find_replace_value: String::new(),
find_search_id: widget::Id::unique(),
find_search_value: String::new(),
git_project_status: None,
projects: Vec::new(),
project_search_id: widget::Id::unique(),
@ -1021,6 +1058,25 @@ impl Application for App {
Some(&self.nav_model)
}
fn on_context_drawer(&mut self) -> Command<Message> {
// Focus correct widget
self.update_focus()
}
//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_opt.is_some() {
// Close find if open
self.find_opt = None;
}
// Focus correct widget
self.update_focus()
}
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Message> {
// Toggle open state and get clone of node data
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
@ -1160,6 +1216,38 @@ impl Application for App {
log::warn!("failed to find font with index {}", index);
}
},
Message::Find(find_opt) => {
self.find_opt = find_opt;
// Focus correct input
return self.update_focus();
}
Message::FindNext => {
if !self.find_search_value.is_empty() {
if let Some(Tab::Editor(tab)) = self.active_tab() {
tab.search(&self.find_search_value, true);
}
}
// Focus correct input
return self.update_focus();
}
Message::FindPrevious => {
if !self.find_search_value.is_empty() {
if let Some(Tab::Editor(tab)) = self.active_tab() {
tab.search(&self.find_search_value, false);
}
}
// Focus correct input
return self.update_focus();
}
Message::FindReplaceValueChanged(value) => {
self.find_replace_value = value;
}
Message::FindSearchValueChanged(value) => {
self.find_search_value = value;
}
Message::GitProjectStatus(project_status) => {
self.git_project_status = Some(project_status);
}
@ -1384,8 +1472,8 @@ impl Application for App {
Message::ProjectSearchResult(project_search_result) => {
self.project_search_result = Some(project_search_result);
// Ensure input remains focused
return widget::text_input::focus(self.project_search_id.clone());
// Focus correct input
return self.update_focus();
}
Message::ProjectSearchSubmit => {
//TODO: Figure out length requirements?
@ -1595,13 +1683,12 @@ impl Application for App {
|x| x,
);
}
ContextPage::ProjectSearch => {
// Ensure focus of correct input
return widget::text_input::focus(self.project_search_id.clone());
}
_ => {}
}
}
// Ensure focus of correct input
return self.update_focus();
}
Message::ToggleLineNumbers => {
self.config.line_numbers = !self.config.line_numbers;
@ -1713,23 +1800,22 @@ impl Application for App {
}
};
let mut text_box = text_box(&tab.editor, self.config.metrics())
.id(self.text_box_id.clone())
.on_changed(Message::TabChanged(tab_id))
.has_context_menu(tab.context_menu.is_some())
.on_context_menu(move |position_opt| {
Message::TabContextMenu(tab_id, position_opt)
});
if self.config.line_numbers {
text_box = text_box.line_numbers();
}
let tab_element: Element<'_, Message> = match tab.context_menu {
Some(position) => widget::popover(
text_box.context_menu(position),
menu::context_menu(&self.config, tab_id),
)
.position(position)
.into(),
None => text_box.into(),
let mut popover =
widget::popover(text_box, menu::context_menu(&self.config, tab_id));
popover = match tab.context_menu {
Some(position) => popover.position(position),
None => popover.show_popup(false),
};
tab_column = tab_column.push(tab_element);
tab_column = tab_column.push(popover);
tab_column = tab_column.push(text(status).font(Font::MONOSPACE));
}
Some(Tab::GitDiff(tab)) => {
@ -1789,6 +1875,49 @@ impl Application for App {
None => {}
}
if let Some(replace) = &self.find_opt {
let text_input =
widget::text_input::text_input(fl!("find-placeholder"), &self.find_search_value)
.id(self.find_search_id.clone())
.on_input(Message::FindSearchValueChanged)
//TODO: shift+enter for FindPrevious
.on_submit(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![
text_input.into(),
button(icon_cache_get("go-up-symbolic", 16))
.on_press(Message::FindPrevious)
.padding(space_xxs)
.style(style::Button::Icon)
.into(),
button(icon_cache_get("go-down-symbolic", 16))
.on_press(Message::FindNext)
.padding(space_xxs)
.style(style::Button::Icon)
.into(),
widget::horizontal_space(Length::Fill).into(),
button(icon_cache_get("window-close-symbolic", 16))
.on_press(Message::Find(None))
.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:

View file

@ -197,8 +197,8 @@ pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> {
menu_item(fl!("paste"), Message::Paste),
menu_item(fl!("select-all"), Message::SelectAll),
MenuTree::new(horizontal_rule(1)),
menu_key(fl!("find"), "Ctrl + F", Message::Todo),
menu_key(fl!("replace"), "Ctrl + H", Message::Todo),
menu_item(fl!("find"), Message::Find(Some(false))),
menu_item(fl!("replace"), Message::Find(Some(true))),
menu_item(
fl!("find-in-project"),
Message::ToggleContextPage(ContextPage::ProjectSearch),

View file

@ -199,4 +199,58 @@ impl EditorTab {
fl!("new-document")
}
}
// Code adapted from cosmic-text ViEditor search
pub fn search(&self, value: &str, forwards: bool) -> bool {
let mut editor = self.editor.lock().unwrap();
let mut cursor = editor.cursor();
let start_line = cursor.line;
if forwards {
while cursor.line < editor.with_buffer(|buffer| buffer.lines.len()) {
if let Some(index) = editor.with_buffer(|buffer| {
buffer.lines[cursor.line]
.text()
.match_indices(value)
.filter_map(|(i, _)| {
if cursor.line != start_line || i > cursor.index {
Some(i)
} else {
None
}
})
.next()
}) {
cursor.index = index;
editor.set_cursor(cursor);
return true;
}
cursor.line += 1;
}
} else {
cursor.line += 1;
while cursor.line > 0 {
cursor.line -= 1;
if let Some(index) = editor.with_buffer(|buffer| {
buffer.lines[cursor.line]
.text()
.rmatch_indices(value)
.filter_map(|(i, _)| {
if cursor.line != start_line || i < cursor.index {
Some(i)
} else {
None
}
})
.next()
}) {
cursor.index = index;
editor.set_cursor(cursor);
return true;
}
}
}
false
}
}

View file

@ -12,7 +12,11 @@ use cosmic::{
image,
layout::{self, Layout},
renderer::{self, Quad},
widget::{self, tree, Widget},
widget::{
self,
operation::{self, Operation, OperationOutputWrapper},
tree, Id, Widget,
},
Shell,
},
};
@ -29,10 +33,11 @@ use crate::{line_number::LineNumberKey, FONT_SYSTEM, LINE_NUMBER_CACHE, SWASH_CA
pub struct TextBox<'a, Message> {
editor: &'a Mutex<ViEditor<'static, 'static>>,
metrics: Metrics,
id: Option<Id>,
padding: Padding,
on_changed: Option<Message>,
click_timing: Duration,
context_menu: Option<Point>,
has_context_menu: bool,
on_context_menu: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
line_numbers: bool,
}
@ -45,15 +50,21 @@ where
Self {
editor,
metrics,
id: None,
padding: Padding::new(0.0),
on_changed: None,
click_timing: Duration::from_millis(500),
context_menu: None,
has_context_menu: false,
on_context_menu: None,
line_numbers: false,
}
}
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
@ -69,8 +80,8 @@ where
self
}
pub fn context_menu(mut self, position: Point) -> Self {
self.context_menu = Some(position);
pub fn has_context_menu(mut self, has_context_menu: bool) -> Self {
self.has_context_menu = has_context_menu;
self
}
@ -252,6 +263,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(
&self,
tree: &widget::Tree,
@ -611,79 +634,92 @@ where
Event::Keyboard(KeyEvent::KeyPressed {
key_code,
modifiers,
}) => 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;
}
KeyCode::Tab => {
if modifiers.shift() {
editor.action(Action::Unindent);
} else {
editor.action(Action::Indent);
}) => {
// Only parse keys when focused
if state.is_focused {
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;
}
KeyCode::Tab => {
if modifiers.shift() {
editor.action(Action::Unindent);
} else {
editor.action(Action::Indent);
}
status = Status::Captured;
}
_ => (),
}
status = Status::Captured;
}
_ => (),
},
}
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
state.modifiers = modifiers;
}
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
// Only parse keys when Super, Ctrl, and Alt are not pressed
if !state.modifiers.logo() && !state.modifiers.control() && !state.modifiers.alt() {
if !character.is_control() {
editor.action(Action::Insert(character));
// Only parse keys when focused
if state.is_focused {
// Only parse keys when Super, Ctrl, and Alt are not pressed
if !state.modifiers.logo()
&& !state.modifiers.control()
&& !state.modifiers.alt()
{
if !character.is_control() {
editor.action(Action::Insert(character));
}
status = Status::Captured;
}
status = Status::Captured;
}
}
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_logical = p.x - self.padding.left;
@ -746,12 +782,13 @@ where
// Update context menu state
if let Some(on_context_menu) = &self.on_context_menu {
shell.publish((on_context_menu)(match self.context_menu {
Some(_) => None,
None => match button {
shell.publish((on_context_menu)(if self.has_context_menu {
None
} else {
match button {
Button::Right => Some(p),
_ => None,
},
}
}));
}
@ -869,6 +906,7 @@ pub struct State {
click: Option<(ClickKind, Instant)>,
dragging: Option<Dragging>,
editor_offset_x: Cell<i32>,
is_focused: bool,
scale_factor: Cell<f32>,
scroll_pixels: f32,
scrollbar_rect: Cell<Rectangle<f32>>,
@ -883,6 +921,7 @@ impl State {
click: None,
dragging: None,
editor_offset_x: Cell::new(0),
is_focused: false,
scale_factor: Cell::new(1.0),
scroll_pixels: 0.0,
scrollbar_rect: Cell::new(Rectangle::default()),
@ -890,3 +929,17 @@ impl State {
}
}
}
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;
}
}