From e58f9af141a84388eadd339c003d9365119caa62 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Tue, 14 Jan 2025 16:21:03 +0100 Subject: [PATCH] textview: Allow zooming the text This allows the user to change the text size via keyboard, scrolling, zoom gesture and from the primary menu. --- aardvark-app/src/aardvark.gresource.xml | 2 + aardvark-app/src/components/mod.rs | 3 + .../src/components/zoom_level_selector.rs | 81 +++++++++++++ .../src/components/zoom_level_selector.ui | 57 +++++++++ aardvark-app/src/main.rs | 1 + aardvark-app/src/style.css | 4 - aardvark-app/src/window.rs | 109 +++++++++++++++++- aardvark-app/src/window.ui | 66 ++++++----- 8 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 aardvark-app/src/components/mod.rs create mode 100644 aardvark-app/src/components/zoom_level_selector.rs create mode 100644 aardvark-app/src/components/zoom_level_selector.ui diff --git a/aardvark-app/src/aardvark.gresource.xml b/aardvark-app/src/aardvark.gresource.xml index fc6e8c7..e4bae67 100644 --- a/aardvark-app/src/aardvark.gresource.xml +++ b/aardvark-app/src/aardvark.gresource.xml @@ -2,7 +2,9 @@ window.ui + components/zoom_level_selector.ui gtk/help-overlay.ui style.css + diff --git a/aardvark-app/src/components/mod.rs b/aardvark-app/src/components/mod.rs new file mode 100644 index 0000000..0ec7580 --- /dev/null +++ b/aardvark-app/src/components/mod.rs @@ -0,0 +1,3 @@ +mod zoom_level_selector; + +pub use self::zoom_level_selector::ZoomLevelSelector; diff --git a/aardvark-app/src/components/zoom_level_selector.rs b/aardvark-app/src/components/zoom_level_selector.rs new file mode 100644 index 0000000..202366b --- /dev/null +++ b/aardvark-app/src/components/zoom_level_selector.rs @@ -0,0 +1,81 @@ +/* zoom_level_selector.rs + * + * Copyright 2025 Julian Sparber + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +use std::cell::Cell; + +use adw::subclass::prelude::*; +use gtk::prelude::*; +use gtk::glib; +use sourceview::*; + +mod imp { + use super::*; + + #[derive(Debug, Default, glib::Properties, gtk::CompositeTemplate)] + #[properties(wrapper_type = super::ZoomLevelSelector)] + #[template(resource = "/org/p2panda/aardvark/components/zoom_level_selector.ui")] + pub struct ZoomLevelSelector { + #[template_child] + pub button: TemplateChild, + #[property(get, set)] + zoom_level: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for ZoomLevelSelector { + const NAME: &'static str = "ZoomLevelSelector"; + type Type = super::ZoomLevelSelector; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for ZoomLevelSelector { + fn constructed(&self) { + self.parent_constructed(); + self.obj() + .bind_property("zoom_level", &*self.button, "label") + .sync_create() + .transform_to(|_, zoom_level: f64| Some(format!("{:.0}%", zoom_level * 100.0))) + .build(); + } + } + + impl WidgetImpl for ZoomLevelSelector {} + impl BoxImpl for ZoomLevelSelector {} +} + +glib::wrapper! { + pub struct ZoomLevelSelector(ObjectSubclass) + @extends gtk::Widget, gtk::Box; +} + +impl ZoomLevelSelector { + pub fn new() -> Self { + glib::Object::builder() + .build() + } +} diff --git a/aardvark-app/src/components/zoom_level_selector.ui b/aardvark-app/src/components/zoom_level_selector.ui new file mode 100644 index 0000000..02b95f7 --- /dev/null +++ b/aardvark-app/src/components/zoom_level_selector.ui @@ -0,0 +1,57 @@ + + + + + + diff --git a/aardvark-app/src/main.rs b/aardvark-app/src/main.rs index a33af2a..1afa626 100644 --- a/aardvark-app/src/main.rs +++ b/aardvark-app/src/main.rs @@ -20,6 +20,7 @@ mod application; mod config; +mod components; mod document; mod textbuffer; mod window; diff --git a/aardvark-app/src/style.css b/aardvark-app/src/style.css index a7876c9..610e9c5 100644 --- a/aardvark-app/src/style.css +++ b/aardvark-app/src/style.css @@ -13,7 +13,3 @@ .user-counter { font-weight: bold; } - -.editor { - font-size: 24px; -} diff --git a/aardvark-app/src/window.rs b/aardvark-app/src/window.rs index eeb230e..80e9279 100644 --- a/aardvark-app/src/window.rs +++ b/aardvark-app/src/window.rs @@ -18,18 +18,23 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +use std::cell; + use adw::prelude::AdwDialogExt; use adw::subclass::prelude::*; use gtk::prelude::*; -use gtk::{gio, glib}; +use gtk::{gio, glib, gdk}; use sourceview::*; -use crate::AardvarkTextBuffer; +use crate::{AardvarkTextBuffer, components::ZoomLevelSelector}; + +const BASE_TEXT_FONT_SIZE: f64 = 24.0; mod imp { use super::*; - #[derive(Debug, Default, gtk::CompositeTemplate)] + #[derive(Debug, Default, glib::Properties, gtk::CompositeTemplate)] + #[properties(wrapper_type = super::AardvarkWindow)] #[template(resource = "/org/p2panda/aardvark/window.ui")] pub struct AardvarkWindow { // Template widgets @@ -41,6 +46,12 @@ mod imp { pub open_document_dialog: TemplateChild, #[template_child] pub toast_overlay: TemplateChild, + pub css_provider: gtk::CssProvider, + pub font_size: cell::Cell, + #[property(get, set = Self::set_font_scale, default = 0.0)] + pub font_scale: cell::Cell, + #[property(get, default = 1.0)] + pub zoom_level: cell::Cell, } #[glib::object_subclass] @@ -50,7 +61,43 @@ mod imp { type ParentType = adw::ApplicationWindow; fn class_init(klass: &mut Self::Class) { + ZoomLevelSelector::static_type(); + klass.bind_template(); + + klass.install_action("window.zoom-in", None, |window, _, _| { + window.set_font_scale(window.font_scale() + 1.0); + }); + klass.install_action("window.zoom-out", None, |window, _, _| { + window.set_font_scale(window.font_scale() - 1.0); + }); + klass.install_action("window.zoom-one", None, |window, _, _| { + window.set_font_scale(0.0); + }); + + klass.add_binding_action(gdk::Key::plus, + gdk::ModifierType::CONTROL_MASK, + "window.zoom-in"); + klass.add_binding_action(gdk::Key::KP_Add, + gdk::ModifierType::CONTROL_MASK, + "window.zoom-in"); + klass.add_binding_action(gdk::Key::minus, + gdk::ModifierType::CONTROL_MASK, + "window.zoom-out"); + // gnome-text-editor uses this as well: probably to make it + // nicer for the US keyboard layout + klass.add_binding_action(gdk::Key::equal, + gdk::ModifierType::CONTROL_MASK, + "window.zoom-out"); + klass.add_binding_action(gdk::Key::KP_Subtract, + gdk::ModifierType::CONTROL_MASK, + "window.zoom-out"); + klass.add_binding_action(gdk::Key::_0, + gdk::ModifierType::CONTROL_MASK, + "window.zoom-one"); + klass.add_binding_action(gdk::Key::KP_0, + gdk::ModifierType::CONTROL_MASK, + "window.zoom-one"); } fn instance_init(obj: &glib::subclass::InitializingObject) { @@ -58,6 +105,7 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for AardvarkWindow { fn constructed(&self) { self.parent_constructed(); @@ -65,6 +113,47 @@ mod imp { let buffer = AardvarkTextBuffer::new(); self.text_view.set_buffer(Some(&buffer)); + self.font_size.set(BASE_TEXT_FONT_SIZE); + self.obj().set_font_scale(0.0); + gtk::style_context_add_provider_for_display ( + &self.obj().display(), + &self.css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); + + let scroll_controller = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL); + scroll_controller.set_propagation_phase(gtk::PropagationPhase::Capture); + let window = self.obj().clone(); + scroll_controller.connect_scroll(move |scroll, _dx, dy| { + if scroll.current_event_state().contains(gdk::ModifierType::CONTROL_MASK) { + if dy < 0.0 { + window.set_font_scale(window.font_scale() + 1.0); + } else { + window.set_font_scale(window.font_scale() - 1.0); + } + glib::Propagation::Stop + } else { + glib::Propagation::Proceed + } + }); + self.obj().add_controller(scroll_controller); + + let zoom_gesture = gtk::GestureZoom::new(); + let window = self.obj().clone(); + let prev_delta = std::cell::Cell::new(0.0); + zoom_gesture.connect_scale_changed(move |_, delta| { + if prev_delta.get() == delta { + return; + } + + if prev_delta.get() < delta { + window.set_font_scale(window.font_scale() + delta); + } else { + window.set_font_scale(window.font_scale() - delta); + } + prev_delta.set(delta); + }); + self.obj().add_controller(zoom_gesture); + let window = self.obj().clone(); let dialog = self.open_document_dialog.clone(); self.open_document_button.connect_clicked(move |_| { @@ -73,6 +162,20 @@ mod imp { } } + impl AardvarkWindow { + fn set_font_scale(&self, value: f64) { + let font_size = self.font_size.get(); + + self.font_scale.set(value); + + let size = (font_size + self.obj().font_scale()).max(1.0); + self.zoom_level.set(size / font_size); + self.obj().notify_zoom_level(); + self.css_provider.load_from_string(&format!( ".sourceview {{ font-size: {size}px; }}")); + self.obj().action_set_enabled("window.zoom-out", size > 1.0); + } + } + impl WidgetImpl for AardvarkWindow {} impl WindowImpl for AardvarkWindow {} impl ApplicationWindowImpl for AardvarkWindow {} diff --git a/aardvark-app/src/window.ui b/aardvark-app/src/window.ui index 34c24db..734aa41 100644 --- a/aardvark-app/src/window.ui +++ b/aardvark-app/src/window.ui @@ -2,12 +2,33 @@ + +
+ + focus-level + +
+
+ + _Preferences + app.preferences + + + _Keyboard Shortcuts + win.show-help-overlay + + + _About Aardvark + app.about + +
+