From 04f2812f4e548d360f471c0e659ef2b97e22341b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 5 Feb 2025 15:28:38 -0700 Subject: [PATCH] Implement drag scroll, fixes #154 --- src/main.rs | 32 +++++++++++++- src/tab.rs | 108 +++++++++++++++++++++++++----------------------- src/text_box.rs | 24 +++++++++++ 3 files changed, 110 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index 77dea1a..672e1e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use cosmic::{ cosmic_theme, executor, font::Font, iced::{ + self, advanced::graphics::text::font_system, clipboard, event, futures::{self, SinkExt}, @@ -318,6 +319,7 @@ enum NewTab { #[derive(Clone, Debug)] pub enum Message { AppTheme(AppTheme), + AutoScroll(Option), Config(Config), ConfigState(ConfigState), CloseFile, @@ -374,6 +376,7 @@ pub enum Message { SaveAll, SaveAsDialog(Option), SaveAsResult(segmented_button::Entity, DialogResult), + Scroll(f32), SelectAll, SystemThemeModeChange(cosmic_theme::ThemeMode), SyntaxTheme(usize, bool), @@ -438,6 +441,7 @@ pub struct App { theme_names: Vec, context_page: ContextPage, text_box_id: widget::Id, + auto_scroll: Option, dialog_opt: Option>, dialog_page_opt: Option, find_opt: Option, @@ -1323,6 +1327,7 @@ impl Application for App { theme_names, context_page: ContextPage::Settings, text_box_id: widget::Id::unique(), + auto_scroll: None, dialog_opt: None, dialog_page_opt: None, find_opt: None, @@ -1566,6 +1571,9 @@ impl Application for App { self.config.app_theme = app_theme; return self.save_config(); } + Message::AutoScroll(auto_scroll) => { + self.auto_scroll = auto_scroll; + } Message::Config(config) => { if config != self.config { log::info!("update config"); @@ -2330,6 +2338,16 @@ impl Application for App { editor.set_selection(selection); } } + Message::Scroll(auto_scroll) => { + if let Some(Tab::Editor(tab)) = self.active_tab_mut() { + let mut editor = tab.editor.lock().unwrap(); + editor.with_buffer_mut(|buffer| { + let mut scroll = buffer.scroll(); + scroll.vertical += auto_scroll; + buffer.set_scroll(scroll); + }); + } + } Message::SystemThemeModeChange(_theme_mode) => { return self.update_config(); } @@ -2687,6 +2705,7 @@ impl Application for App { Some(Tab::Editor(tab)) => { let mut text_box = text_box(&tab.editor, self.config.metrics()) .id(self.text_box_id.clone()) + .on_auto_scroll(Message::AutoScroll) .on_changed(Message::TabChanged(tab_id)) .has_context_menu(tab.context_menu.is_some()) .on_context_menu(move |position_opt| { @@ -2928,7 +2947,7 @@ impl Application for App { struct ConfigStateSubscription; struct ThemeSubscription; - Subscription::batch([ + let mut subscriptions = vec![ event::listen_with(|event, status, window_id| match event { event::Event::Keyboard(keyboard::Event::KeyPressed { modifiers, key, .. }) => { match status { @@ -3046,6 +3065,15 @@ impl Application for App { Some(dialog) => dialog.subscription(), None => Subscription::none(), }, - ]) + ]; + + if let Some(auto_scroll) = self.auto_scroll { + subscriptions.push( + iced::time::every(time::Duration::from_millis(10)) + .map(move |_| Message::Scroll(auto_scroll)), + ); + } + + Subscription::batch(subscriptions) } } diff --git a/src/tab.rs b/src/tab.rs index 86c4524..132b8db 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -9,14 +9,13 @@ use cosmic_text::{Attrs, Buffer, Cursor, Edit, Selection, Shaping, SyntaxEditor, use notify::Watcher; use regex::Regex; use std::{ - io::Write, fs, + io::Write, path::PathBuf, process::{Command, Stdio}, sync::{Arc, Mutex}, }; - use crate::{fl, git::GitDiff, Config, SYNTAX_SYSTEM}; pub enum Tab { @@ -147,65 +146,70 @@ impl EditorTab { } pub fn save(&mut self) { - if let Some(path) = &self.path_opt { - let mut 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_str(line.ending().as_str()); - } - }); + if let Some(path) = &self.path_opt { + let mut editor = self.editor.lock().unwrap(); + let mut text = String::new(); - match fs::write(path, &text) { - Ok(()) => { - editor.save_point(); - log::info!("saved {:?}", path); - } - Err(err) => { - if err.kind() == std::io::ErrorKind::PermissionDenied { - log::warn!("Permission denied. Attempting to save with pkexec."); - - if let Ok(mut output) = Command::new("pkexec") - .arg("tee") - .arg(path) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) // Redirect stdout to /dev/null - .stderr(Stdio::inherit()) // Retain stderr for error visibility - .spawn() - { - if let Some(mut stdin) = output.stdin.take() { - if let Err(e) = stdin.write_all(text.as_bytes()) { - log::error!("Failed to write to stdin: {}", e); - } - } else { - log::error!("Failed to access stdin of pkexec process."); - } + editor.with_buffer(|buffer| { + for line in buffer.lines.iter() { + text.push_str(line.text()); + text.push_str(line.ending().as_str()); + } + }); - // Ensure the child process is reaped - match output.wait() { - Ok(status) => { - if status.success() { - // Mark the editor's state as saved if the process succeeds - editor.save_point(); - log::info!("File saved successfully with pkexec."); - } else { - log::error!("pkexec process exited with a non-zero status: {:?}", status); + match fs::write(path, &text) { + Ok(()) => { + editor.save_point(); + log::info!("saved {:?}", path); + } + Err(err) => { + if err.kind() == std::io::ErrorKind::PermissionDenied { + log::warn!("Permission denied. Attempting to save with pkexec."); + + if let Ok(mut output) = Command::new("pkexec") + .arg("tee") + .arg(path) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) // Redirect stdout to /dev/null + .stderr(Stdio::inherit()) // Retain stderr for error visibility + .spawn() + { + if let Some(mut stdin) = output.stdin.take() { + if let Err(e) = stdin.write_all(text.as_bytes()) { + log::error!("Failed to write to stdin: {}", e); } + } else { + log::error!("Failed to access stdin of pkexec process."); } - Err(e) => { - log::error!("Failed to wait on pkexec process: {}", e); + + // Ensure the child process is reaped + match output.wait() { + Ok(status) => { + if status.success() { + // Mark the editor's state as saved if the process succeeds + editor.save_point(); + log::info!("File saved successfully with pkexec."); + } else { + log::error!( + "pkexec process exited with a non-zero status: {:?}", + status + ); + } + } + Err(e) => { + log::error!("Failed to wait on pkexec process: {}", e); + } } + } else { + log::error!( + "Failed to spawn pkexec process. Check permissions or path." + ); } - } else { - log::error!("Failed to spawn pkexec process. Check permissions or path."); } } } - } - } else { - log::warn!("tab has no path yet"); + } else { + log::warn!("tab has no path yet"); } } diff --git a/src/text_box.rs b/src/text_box.rs index b30ffe0..0dd0b11 100644 --- a/src/text_box.rs +++ b/src/text_box.rs @@ -42,6 +42,7 @@ pub struct TextBox<'a, Message> { metrics: Metrics, id: Option, padding: Padding, + on_auto_scroll: Option) -> Message + 'a>>, on_changed: Option, click_timing: Duration, has_context_menu: bool, @@ -60,6 +61,7 @@ where metrics, id: None, padding: Padding::new(0.0), + on_auto_scroll: None, on_changed: None, click_timing: Duration::from_millis(500), has_context_menu: false, @@ -79,6 +81,11 @@ where self } + pub fn on_auto_scroll(mut self, on_auto_scroll: impl Fn(Option) -> Message + 'a) -> Self { + self.on_auto_scroll = Some(Box::new(on_auto_scroll)); + self + } + pub fn on_changed(mut self, on_changed: Message) -> Self { self.on_changed = Some(on_changed); self @@ -1109,6 +1116,9 @@ where Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => { state.dragging = None; status = Status::Captured; + if let Some(on_auto_scroll) = &self.on_auto_scroll { + shell.publish(on_auto_scroll(None)); + } } Event::Mouse(MouseEvent::CursorMoved { .. }) => { if let Some(dragging) = &state.dragging { @@ -1124,6 +1134,20 @@ where x: x as i32, y: y as i32, }); + let auto_scroll = editor.with_buffer(|buffer| { + //TODO: ideal auto scroll speed + let speed = 10.0; + if y < 0.0 { + Some(y * speed) + } else if y > buffer.size().1.unwrap_or(0.0) { + Some((y - buffer.size().1.unwrap_or(0.0)) * speed) + } else { + None + } + }); + if let Some(on_auto_scroll) = &self.on_auto_scroll { + shell.publish(on_auto_scroll(auto_scroll)); + } } Dragging::ScrollbarV { start_y,