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 @@
+
+
+
+
+ True
+
+
+
+
+
+
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 @@
+
Aardvark
800
600
@@ -30,7 +51,16 @@
True
open-menu-symbolic
Main Menu
- primary_menu
+
+
+
@@ -107,7 +137,7 @@
Share Document
@@ -128,8 +158,8 @@
25
356702
@@ -141,7 +171,7 @@
@@ -158,22 +188,6 @@
-
true
320
@@ -182,7 +196,7 @@
-
+
@@ -199,7 +213,7 @@
Open Document
@@ -241,8 +255,8 @@
Open
140