diff --git a/Cargo.lock b/Cargo.lock
index 2dd68f7..ca905f1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1974,9 +1974,9 @@ dependencies = [
[[package]]
name = "num-traits"
-version = "0.2.17"
+version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
@@ -2895,6 +2895,7 @@ name = "terms"
version = "0.1.0"
dependencies = [
"anyhow",
+ "approx",
"ashpd",
"async-channel 2.1.1",
"async-std",
@@ -2914,6 +2915,7 @@ dependencies = [
"libadwaita",
"libc",
"librsvg",
+ "num-traits",
"once_cell",
"pango",
"rand",
diff --git a/Cargo.toml b/Cargo.toml
index 941959e..f609e20 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,6 +58,8 @@ libc = "0.2.153"
thiserror = "1.0.56"
anyhow = "1.0.79"
zbus = "3.14.1"
+num-traits = "0.2.18"
+approx = "0.5.1"
[build-dependencies]
glib-build-tools = "0.19.0"
diff --git a/data/resources/meson.build b/data/resources/meson.build
index b260fa7..7ffdb3f 100644
--- a/data/resources/meson.build
+++ b/data/resources/meson.build
@@ -1,6 +1,30 @@
# Resources
+fs = import('fs')
subdir('icons')
+# stylesheet_deps = []
+# sassc = find_program('sassc')
+
+# if sassc.found()
+# sassc_opts = [ '-a', '-M', '-t', 'compact' ]
+
+# scss_files = [
+# 'style',
+# 'style-dark',
+# ]
+
+# foreach scss: scss_files
+# stylesheet_deps += custom_target('@0@.scss'.format(scss),
+# input: '@0@.scss'.format(scss),
+# output: '@0@.css'.format(scss),
+# command: [
+# sassc, sassc_opts, '@INPUT@', '@OUTPUT@',
+# ],
+# )
+# endforeach
+# endif
+
+
resources = gnome.compile_resources(
'resources',
'resources.gresource.xml',
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 595fac8..e13ee1a 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -34,6 +34,7 @@
+ ../../src/twl/panel_header.ui
../../src/twl/zoom_controls.ui
../../src/twl/style_switcher.ui
diff --git a/data/resources/style.css b/data/resources/style.css
index 151e49f..bc0b7c9 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -3,6 +3,7 @@
@define-color root_context_color shade(@red_1, 1.38);
@define-color ssh_context_color shade(@purple_1, 1.28);
+/* @define-color panel_border_color alpha(@shade_color, 1); */
/* terms_main_windowwith-borders:not(.fullscreen):backdrop {
border-color: alpha(@borders, 0.5);
@@ -22,6 +23,7 @@ window:not(.about) headerbar,
margin-bottom: -6px;
}
+
/* #terms_main_window.context-root .custom-headerbar {
background-color: @root_context_color;
}
@@ -83,11 +85,20 @@ window:not(.about) headerbar,
padding: 12px;
}
-#twl_style_switcher {
+
+
+/******************************** TWL ************************************/
+
+panel_grid paned separator {
+ background-color: @headerbar_bg_color;
+ border-color: @headerbar_bg_color;
+}
+
+style_switcher {
padding: 6px;
}
-#twl_style_switcher .check {
+style_switcher .check {
background: @accent_bg_color;
color: @accent_fg_color;
padding: 2px;
@@ -97,12 +108,12 @@ window:not(.about) headerbar,
/* Adapted from https://gitlab.gnome.org/GNOME/gnome-text-editor/-/blob/bf8c0c249f06a0be69e65aed3b786ba02a9f999e/src/TextEditor.css#L51 */
-#twl_style_switcher checkbutton {
+style_switcher checkbutton {
outline-offset: 1px;
transition: none;
}
-#twl_style_switcher checkbutton radio {
+style_switcher checkbutton radio {
-gtk-icon-source: none;
background: none;
padding: 12px;
@@ -114,25 +125,37 @@ window:not(.about) headerbar,
box-shadow: inset 0 0 0 1px @borders;
}
-#twl_style_switcher checkbutton radio:checked {
+style_switcher checkbutton radio:checked {
box-shadow: inset 0 0 0 2px @accent_bg_color;
}
-#twl_style_switcher checkbutton.system radio {
+style_switcher checkbutton.system radio {
background: linear-gradient(-45deg, #1e1e1e 49.99%, white 50.01%);
}
-#twl_style_switcher checkbutton.light radio {
+style_switcher checkbutton.light radio {
color: alpha(black, 0.8);
background-color: white;
}
-#twl_style_switcher checkbutton.dark radio {
+style_switcher checkbutton.dark radio {
color: white;
background-color: #1e1e1e;
}
-paned separator {
- background-color: @window_bg_color;
- border-color: @window_bg_color;
+
+panel_header {
+ -gtk-icon-size: 12px;
+}
+
+
+panel_header.toolbar {
+ padding: 0px;
+ border-spacing: 0px;
+}
+
+
+panel_header button {
+ min-height: 16px;
+ min-width: 16px;
}
diff --git a/src/components/terminal_tab/terminal_tab.rs b/src/components/terminal_tab/terminal_tab.rs
index 6b16a93..d56cc40 100644
--- a/src/components/terminal_tab/terminal_tab.rs
+++ b/src/components/terminal_tab/terminal_tab.rs
@@ -127,6 +127,11 @@ impl TerminalTab {
}));
self.panel_grid.set_wide_handle(self.settings.use_wide_panel_resize_handle());
+ self.settings.connect_show_panel_headers_changed(clone!(@weak self as this => move |s| {
+ this.panel_grid.set_show_panel_headers(s.show_panel_headers());
+ }));
+ self.panel_grid.set_show_panel_headers(self.settings.show_panel_headers());
+
self.panel_grid.connect_selected_panel_notify(clone!(@weak self as this => move |s| {
this.on_selected_panel_change();
}));
@@ -163,7 +168,7 @@ impl TerminalTab {
}
fn get_selected(&self) -> Option {
- self.panel_grid.selected_panel().and_then(|p| p.child()).and_downcast()
+ self.panel_grid.selected_panel().and_then(|p| p.content()).and_downcast()
}
fn set_selected(&self, terminal: Option) {
@@ -176,7 +181,7 @@ impl TerminalTab {
debug!("on panel changed: {:?}", panel);
self.selected_panel_signals.set_target(panel.as_ref());
if let Some(panel) = panel.as_ref() {
- let term = panel.child().and_downcast::();
+ let term = panel.content().and_downcast::();
debug!("Set active term {:?}", term);
self.active_term_signals.set_target(term.as_ref());
if let Some(term) = term.as_ref() {
@@ -188,7 +193,7 @@ impl TerminalTab {
pub fn split(&self, orientation: Option) {
let term = Terminal::new(self.directory.borrow().clone(), self.command.borrow().clone(), self.env.borrow().clone());
- term.grab_focus();
+ // term.grab_focus();
let panel = self.panel_grid.split(&term, orientation);
self.connect_terminal_signals(&term, &panel);
@@ -201,14 +206,14 @@ impl TerminalTab {
}));
terminal.connect_title_notify(clone!(@weak self as this, @weak panel as panel => move |term| {
- panel.set_title(term.title());
+ panel.header().set_title(term.title());
}));
}
pub fn on_panel_close_request(&self, panel: &Panel) -> glib::Propagation {
info!("on_panel_close_request: {:?}", panel);
// TODO: test if process is still running
- if let Some(terminal) = panel.child().and_downcast_ref::() {}
+ if let Some(terminal) = panel.content().and_downcast_ref::() {}
glib::Propagation::Proceed
}
diff --git a/src/twl/bin.rs b/src/twl/bin.rs
new file mode 100644
index 0000000..458cbac
--- /dev/null
+++ b/src/twl/bin.rs
@@ -0,0 +1,22 @@
+use glib::{prelude::*, subclass::prelude::*};
+
+use super::{bin_imp as imp, utils::TwlWidgetExt};
+
+glib::wrapper! {
+ pub struct Bin(ObjectSubclass)
+ @extends gtk::Widget, adw::Bin,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+}
+
+impl Bin {
+ pub fn new(child: &impl IsA) -> Self {
+ glib::Object::builder().property("child", child).build()
+ }
+}
+
+impl Default for Bin {
+ fn default() -> Self {
+ glib::Object::builder().build()
+ }
+}
+impl TwlWidgetExt for Bin {}
diff --git a/src/twl/bin_imp.rs b/src/twl/bin_imp.rs
new file mode 100644
index 0000000..c5b6024
--- /dev/null
+++ b/src/twl/bin_imp.rs
@@ -0,0 +1,374 @@
+use std::cmp::Ordering;
+
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use glib;
+use gtk::graphene;
+
+use approx::abs_diff_eq;
+use num_traits as num;
+
+use super::utils::{Orthogonal, TwlWidgetExt};
+
+#[derive(Debug, Default)]
+pub struct Bin {}
+
+#[glib::object_subclass]
+impl ObjectSubclass for Bin {
+ const NAME: &'static str = "TwlBin";
+ type Type = super::Bin;
+ type ParentType = adw::Bin;
+}
+
+impl ObjectImpl for Bin {
+ fn constructed(&self) {
+ self.parent_constructed();
+ }
+}
+
+impl WidgetImpl for Bin {
+ fn focus(&self, direction: gtk::DirectionType) -> bool {
+ let focus_child = self.obj().focus_child();
+
+ let mut ret = false;
+ for child in self.focus_sort(direction.clone()).into_iter() {
+ if focus_child.as_ref() == Some(&child) {
+ ret = child.child_focus(direction.clone());
+ } else if child.is_mapped() && child.is_ancestor(&*self.obj()) {
+ ret = child.child_focus(direction.clone());
+ }
+ }
+ ret
+ }
+
+ fn grab_focus(&self) -> bool {
+ for child in self.obj().iter_children() {
+ if child.grab_focus() {
+ return true;
+ }
+ }
+ false
+ }
+}
+
+impl BinImpl for Bin {}
+
+impl Bin {
+ fn old_focus_coords(&self) -> Option {
+ self.obj()
+ .root()
+ .and_then(|r| r.focus())
+ .and_then(|old_focus| old_focus.compute_bounds(self.obj().as_ref()))
+ }
+
+ /// Look for a child in @children that is intermediate between the focus widget
+ /// and container. This widget, if it exists, acts as the starting widget for
+ /// focus navigation.
+ fn find_old_focus(&self, children: &mut Vec) -> Option {
+ for child in children {
+ let mut test_child = child.clone();
+ let mut found = true;
+ while let Some(parent) = test_child.parent() {
+ if parent == *self.obj() {
+ break;
+ }
+
+ if let Some(focus_child) = parent.focus_child() {
+ if focus_child != *self.obj() {
+ found = false;
+ break;
+ }
+ }
+
+ test_child = parent;
+ }
+
+ if found {
+ return Some(child.clone());
+ }
+ }
+
+ None
+ }
+
+ pub fn focus_sort_tab(&self, children: &mut Vec, direction: gtk::DirectionType) {
+ let text_direction = self.obj().direction();
+ children.sort_by(|child1, child2| {
+ let child_bounds1 = child1.parent().and_then(|p1| child1.compute_bounds(&p1));
+ let child_bounds2 = child2.parent().and_then(|p2| child1.compute_bounds(&p2));
+
+ if child_bounds1.is_none() || child_bounds2.is_none() {
+ return Ordering::Equal;
+ }
+
+ let child_bounds1 = child_bounds1.unwrap();
+ let child_bounds2 = child_bounds2.unwrap();
+
+ let y1 = child_bounds1.y() as f64 + (child_bounds1.height() as f64 / 2.0);
+ let y2 = child_bounds2.y() as f64 + (child_bounds2.height() as f64 / 2.0);
+
+ if abs_diff_eq!(y1, y2) {
+ let x1 = child_bounds1.x() as f64 + (child_bounds1.width() as f64 / 2.0);
+ let x2 = child_bounds2.x() as f64 + (child_bounds2.width() as f64 / 2.0);
+
+ let mut inv = if text_direction == gtk::TextDirection::Rtl { -1 } else { 1 };
+
+ if direction == gtk::DirectionType::TabBackward {
+ inv = inv * -1;
+ }
+
+ let ordering = if x1 < x2 {
+ -1 * inv
+ } else if abs_diff_eq!(x1, x2) {
+ 0
+ } else {
+ inv
+ };
+
+ ordering.cmp(&0)
+ } else {
+ let mut ordering = if y1 < y2 { -1 } else { 1 };
+
+ if direction == gtk::DirectionType::TabBackward {
+ ordering = ordering * -1;
+ }
+ ordering.cmp(&0)
+ }
+ })
+ }
+
+ pub fn focus_sort_left_right(&self, children: &mut Vec, direction: gtk::DirectionType) {
+ let old_focus = self.obj().focus_child().or_else(|| self.find_old_focus(children));
+
+ let old_bounds = old_focus.as_ref().and_then(|w| w.compute_bounds(self.obj().as_ref()));
+
+ let (compare_x, compare_y) = if let (Some(old_focus), Some(old_bounds)) = (old_focus, old_bounds) {
+ // Delete widgets from list that don't match minimum criteria
+ let compare_y1 = old_bounds.y();
+ let compare_y2 = old_bounds.y() + old_bounds.height();
+
+ let compare_x = if direction == gtk::DirectionType::Left {
+ old_bounds.x()
+ } else {
+ old_bounds.x() + old_bounds.width()
+ };
+
+ *children = children
+ .iter()
+ .filter(|child| {
+ if *child != &old_focus {
+ if let Some(child_bounds) = child.compute_bounds(self.obj().as_ref()) {
+ let child_y1 = child_bounds.y();
+ let child_y2 = child_bounds.y() + child_bounds.height();
+
+ if abs_diff_eq!(child_y2, compare_y1) || child_y2 < compare_y1 ||
+ abs_diff_eq!(child_y1, compare_y2) || child_y1 > compare_y2 /* No vertical overlap */ ||
+ (direction == gtk::DirectionType::Right && (child_bounds.x() + child_bounds.width()) < compare_x) || /* Not to left */
+ (direction == gtk::DirectionType::Left && (child_bounds.x() > compare_x))
+ /* Not to right */
+ {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ true
+ })
+ .cloned()
+ .collect();
+ (old_bounds.x() + (old_bounds.width() / 2.0), (compare_y1 + compare_y2) / 2.0)
+ } else {
+ // No old focus widget, need to figure out starting x,y some other way
+
+ let bounds = self
+ .obj()
+ .compute_bounds(self.obj().parent().as_ref().unwrap_or(self.obj().upcast_ref()))
+ .unwrap_or(graphene::Rect::new(0.0, 0.0, 0.0, 0.0));
+ let compare_y = if let Some(old_focus_bounds) = self.old_focus_coords() {
+ old_focus_bounds.y() + (old_focus_bounds.height() / 2.0)
+ } else if self.obj().native().is_none() {
+ bounds.y() + (bounds.height() / 2.0)
+ } else {
+ bounds.height() / 2.0
+ };
+
+ let compare_x = if self.obj().native().is_none() {
+ if direction == gtk::DirectionType::Right {
+ bounds.x()
+ } else {
+ bounds.x() + bounds.width()
+ }
+ } else {
+ if direction == gtk::DirectionType::Left {
+ 0.0
+ } else {
+ bounds.width()
+ }
+ };
+
+ (compare_x, compare_y)
+ };
+
+ let reverse = direction == gtk::DirectionType::Left;
+
+ children.sort_by(|child1, child2| self.axis_compare(child1, child2, compare_x, compare_y, reverse, gtk::Orientation::Horizontal))
+ }
+
+ pub fn focus_sort_up_down(&self, children: &mut Vec, direction: gtk::DirectionType) {
+ let old_focus = self.obj().focus_child().or_else(|| self.find_old_focus(children));
+
+ let old_bounds = old_focus.as_ref().and_then(|w| w.compute_bounds(self.obj().as_ref()));
+ let (compare_x, compare_y) = if let (Some(old_focus), Some(old_bounds)) = (old_focus, old_bounds) {
+ // Delete widgets from list that don't match minimum criteria
+ let compare_x1 = old_bounds.x();
+ let compare_x2 = old_bounds.x() + old_bounds.width();
+
+ let compare_y = if direction == gtk::DirectionType::Up {
+ old_bounds.y()
+ } else {
+ old_bounds.y() + old_bounds.height()
+ };
+
+ *children = children
+ .iter()
+ .filter(|child| {
+ if *child != &old_focus {
+ if let Some(child_bounds) = child.compute_bounds(self.obj().as_ref()) {
+ let child_x1 = child_bounds.x();
+ let child_x2 = child_bounds.x() + child_bounds.width();
+
+ if abs_diff_eq!(child_x2, compare_x1) || child_x2 < compare_x1 ||
+ abs_diff_eq!(child_x1, compare_x2) || child_x1 > compare_x2 /* No horizontal overlap */ ||
+ (direction == gtk::DirectionType::Down && (child_bounds.y() + child_bounds.height()) < compare_y) || /* Not below */
+ (direction == gtk::DirectionType::Up && (child_bounds.y() > compare_y))
+ /* Not above */
+ {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ true
+ })
+ .cloned()
+ .collect();
+ ((compare_x1 + compare_x2) / 2.0, old_bounds.y() + (old_bounds.height() / 2.0))
+ } else {
+ // No old focus widget, need to figure out starting x,y some other way
+
+ let bounds = self
+ .obj()
+ .compute_bounds(self.obj().parent().as_ref().unwrap_or(self.obj().upcast_ref()))
+ .unwrap_or(graphene::Rect::new(0.0, 0.0, 0.0, 0.0));
+ let compare_x = if let Some(old_focus_bounds) = self.old_focus_coords() {
+ old_focus_bounds.x() + (old_focus_bounds.width() / 2.0)
+ } else if self.obj().native().is_none() {
+ bounds.x() + (bounds.width() / 2.0)
+ } else {
+ bounds.width() / 2.0
+ };
+
+ let compare_y = if self.obj().native().is_none() {
+ if direction == gtk::DirectionType::Down {
+ bounds.y()
+ } else {
+ bounds.y() + bounds.height()
+ }
+ } else {
+ if direction == gtk::DirectionType::Down {
+ 0.0
+ } else {
+ bounds.height()
+ }
+ };
+
+ (compare_x, compare_y)
+ };
+
+ let reverse = direction == gtk::DirectionType::Up;
+
+ children.sort_by(|child1, child2| self.axis_compare(child1, child2, compare_x, compare_y, reverse, gtk::Orientation::Vertical))
+ }
+
+ fn focus_sort(&self, direction: gtk::DirectionType) -> Vec {
+ // Initialize the list with all visible child widgets
+ let mut children: Vec = self.obj().iter_children().filter(|c| c.is_mapped() && c.is_sensitive()).collect();
+
+ // Now sort that list depending on @direction
+ match direction {
+ gtk::DirectionType::TabForward | gtk::DirectionType::TabBackward => self.focus_sort_tab(&mut children, direction),
+ gtk::DirectionType::Up | gtk::DirectionType::Down => self.focus_sort_up_down(&mut children, direction),
+ gtk::DirectionType::Left | gtk::DirectionType::Right => self.focus_sort_left_right(&mut children, direction),
+ _ => unreachable!("unknown direction type"),
+ }
+
+ children
+ }
+
+ fn axis_compare(
+ &self,
+ child1: &impl IsA,
+ child2: &impl IsA,
+ x: f32,
+ y: f32,
+ reverse: bool,
+ orientation: gtk::Orientation,
+ ) -> Ordering {
+ let bounds1 = child1.as_ref().compute_bounds(self.obj().as_ref());
+ let bounds2 = child2.as_ref().compute_bounds(self.obj().as_ref());
+
+ if bounds1.is_none() || bounds2.is_none() {
+ return Ordering::Equal;
+ }
+
+ let (mut start1, end1) = axis_info(bounds1.as_ref().unwrap(), orientation);
+ let (mut start2, end2) = axis_info(bounds2.as_ref().unwrap(), orientation);
+
+ start1 = start1 + (end1 / 2.0);
+ start2 = start2 + (end2 / 2.0);
+
+ let (x1, x2) = if start1 == start2 {
+ // Now use origin/bounds to compare the 2 widgets on the other axis
+ let (start1, end1) = axis_info(bounds1.as_ref().unwrap(), orientation.orthogonal());
+ let (start2, end2) = axis_info(bounds2.as_ref().unwrap(), orientation.orthogonal());
+
+ let x1 = num::abs(start1 + (end1 / 2.0) - x);
+ let x2 = num::abs(start2 + (end2 / 2.0) - x);
+
+ (x1, x2)
+ } else {
+ (start1, start2)
+ };
+
+ let inv = if reverse { -1 } else { 1 };
+ let ordering = if x1 < x2 {
+ -1 * inv
+ } else if abs_diff_eq!(x1, x2) {
+ 0
+ } else {
+ inv
+ };
+ ordering.cmp(&0)
+ }
+
+ // gboolean
+ // adw_widget_grab_focus_self (GtkWidget *widget)
+ // {
+ // if (!gtk_widget_get_focusable (widget))
+ // return FALSE;
+
+ // gtk_root_set_focus (gtk_widget_get_root (widget), widget);
+
+ // return TRUE;
+ // }
+}
+
+fn axis_info(bounds: &graphene::Rect, orientation: gtk::Orientation) -> (f32, f32) {
+ match orientation {
+ gtk::Orientation::Horizontal => (bounds.x(), bounds.width()),
+ gtk::Orientation::Vertical => (bounds.y(), bounds.height()),
+ _ => unreachable!(),
+ }
+}
diff --git a/src/twl/fading_label.rs b/src/twl/fading_label.rs
new file mode 100644
index 0000000..5c58fe8
--- /dev/null
+++ b/src/twl/fading_label.rs
@@ -0,0 +1,26 @@
+use glib::{prelude::*, subclass::prelude::*};
+
+use super::{fading_label_imp as imp, utils::TwlWidgetExt};
+
+glib::wrapper! {
+ pub struct FadingLabel(ObjectSubclass)
+ @extends gtk::Widget,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+}
+
+impl FadingLabel {
+ pub fn new(label: Option<&str>) -> Self {
+ let mut builder = glib::Object::builder();
+ if let Some(label) = label {
+ builder = builder.property("label", label);
+ }
+ builder.build()
+ }
+}
+
+impl Default for FadingLabel {
+ fn default() -> Self {
+ glib::Object::builder().build()
+ }
+}
+impl TwlWidgetExt for FadingLabel {}
diff --git a/src/twl/fading_label_imp.rs b/src/twl/fading_label_imp.rs
new file mode 100644
index 0000000..73cf6d0
--- /dev/null
+++ b/src/twl/fading_label_imp.rs
@@ -0,0 +1,195 @@
+use std::{cell::Cell, marker::PhantomData};
+
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use glib::{self, Properties};
+use gtk::{graphene, gsk};
+
+use approx::abs_diff_eq;
+use num_traits as num;
+
+const DEFAULT_FADE_WIDTH: f32 = 18.0;
+
+#[derive(Debug, Properties)]
+#[properties(wrapper_type=super::FadingLabel)]
+pub struct FadingLabel {
+ #[property(get=Self::get_label, set=Self::set_label, construct, explicit_notify)]
+ label: PhantomData,
+
+ #[property(get, set=Self::set_align, minimum=0.0, maximum=1.0, default=0.0, construct, explicit_notify)]
+ align: Cell,
+
+ #[property(get, set=Self::set_fade_width, default=DEFAULT_FADE_WIDTH, construct, explicit_notify)]
+ fade_width: Cell,
+
+ label_widget: gtk::Label,
+}
+
+impl Default for FadingLabel {
+ fn default() -> Self {
+ Self {
+ label_widget: gtk::Label::new(None),
+ label: Default::default(),
+ align: Cell::new(0.0),
+ fade_width: Cell::new(DEFAULT_FADE_WIDTH),
+ }
+ }
+}
+
+#[glib::object_subclass]
+impl ObjectSubclass for FadingLabel {
+ const NAME: &'static str = "TwlFadingLabel";
+ type Type = super::FadingLabel;
+ type ParentType = gtk::Widget;
+}
+
+#[glib::derived_properties]
+impl ObjectImpl for FadingLabel {
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ self.label_widget.set_parent(&*self.obj());
+ self.label_widget.set_single_line_mode(true);
+ }
+
+ fn dispose(&self) {
+ self.label_widget.unparent();
+ }
+}
+
+impl WidgetImpl for FadingLabel {
+ fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
+ let (mut min, nat, min_baseline, nat_baseline) = self.label_widget.measure(orientation, for_size);
+
+ if orientation == gtk::Orientation::Horizontal && min > 0 {
+ min = 0;
+ }
+ (min, nat, min_baseline, nat_baseline)
+ }
+
+ fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
+ let align = if self.is_rtl() { 1.0 - self.align.get() } else { self.align.get() };
+
+ let (_, child_width, _, _) = self.label_widget.measure(gtk::Orientation::Horizontal, height);
+
+ let offset = (width as f32 - child_width as f32) * align;
+ let transform = gsk::Transform::new().translate(&graphene::Point::new(offset, 0.0));
+
+ self.label_widget.allocate(child_width, height, baseline, Some(transform));
+ }
+
+ fn snapshot(&self, snapshot: >k::Snapshot) {
+ let align = if self.is_rtl() { 1.0 - self.align.get() } else { self.align.get() };
+ let width = self.obj().width();
+
+ if width <= 0 {
+ return;
+ }
+
+ let clipped_size = self.label_widget.width() - width;
+
+ if clipped_size <= 0 {
+ self.obj().snapshot_child(&self.label_widget, snapshot);
+ return;
+ }
+
+ let width = width as f32;
+ let child_snapshot = gtk::Snapshot::new();
+ self.obj().snapshot_child(&self.label_widget, &child_snapshot);
+
+ let node = child_snapshot.to_node();
+
+ if node.is_none() {
+ self.obj().snapshot_child(&self.label_widget, snapshot);
+ return;
+ }
+
+ let node = node.unwrap();
+
+ let node_bounds = node.bounds();
+ let bounds = graphene::Rect::new(0.0, node_bounds.y().floor(), width, f32::ceil(node_bounds.height() + 1.0));
+
+ snapshot.push_mask(gsk::MaskMode::InvertedAlpha);
+
+ if align > 0.0 {
+ snapshot.append_linear_gradient(
+ &graphene::Rect::new(0.0, bounds.y(), self.fade_width.get(), bounds.height()),
+ &graphene::Point::new(0.0, 0.0),
+ &graphene::Point::new(self.fade_width.get(), 0.0),
+ &[
+ gsk::ColorStop::new(0.0, gdk::RGBA::new(0.0, 0.0, 0.0, 1.0)),
+ gsk::ColorStop::new(1.0, gdk::RGBA::new(0.0, 0.0, 0.0, 0.0)),
+ ],
+ );
+ }
+
+ if align < 1.0 {
+ snapshot.append_linear_gradient(
+ &graphene::Rect::new(width - self.fade_width.get(), bounds.y(), self.fade_width.get(), bounds.height()),
+ &graphene::Point::new(width, 0.0),
+ &graphene::Point::new(width - self.fade_width.get(), 0.0),
+ &[
+ gsk::ColorStop::new(0.0, gdk::RGBA::new(0.0, 0.0, 0.0, 1.0)),
+ gsk::ColorStop::new(1.0, gdk::RGBA::new(0.0, 0.0, 0.0, 0.0)),
+ ],
+ );
+ }
+
+ snapshot.pop();
+
+ snapshot.push_clip(&bounds);
+ snapshot.append_node(&node);
+ snapshot.pop();
+
+ snapshot.pop();
+ }
+}
+
+impl FadingLabel {
+ fn set_label(&self, label: &str) {
+ if label == &self.get_label() {
+ return;
+ }
+
+ self.label_widget.set_label(label);
+ self.obj().notify_label();
+ }
+
+ fn get_label(&self) -> String {
+ self.label_widget.label().into()
+ }
+
+ fn set_align(&self, align: f32) {
+ let align = num::clamp(align, 0.0, 1.0);
+
+ if abs_diff_eq!(self.align.get(), align) {
+ return;
+ }
+
+ self.align.set(align);
+
+ self.obj().queue_allocate();
+ self.obj().notify_align();
+ }
+
+ fn set_fade_width(&self, fade_width: f32) {
+ if abs_diff_eq!(self.fade_width.get(), fade_width) {
+ return;
+ }
+
+ self.fade_width.set(fade_width);
+
+ self.obj().queue_allocate();
+ self.obj().notify_align();
+ }
+
+ fn is_rtl(&self) -> bool {
+ let direction = pango::find_base_dir(&self.get_label());
+
+ match direction {
+ pango::Direction::Rtl => true,
+ pango::Direction::Ltr => false,
+ _ => self.obj().direction() == gtk::TextDirection::Rtl,
+ }
+ }
+}
diff --git a/src/twl/mod.rs b/src/twl/mod.rs
index 57a88d5..1ba8f02 100644
--- a/src/twl/mod.rs
+++ b/src/twl/mod.rs
@@ -1,6 +1,5 @@
mod zoom_controls;
mod zoom_controls_imp;
-use glib::subclass::SignalInvocationHint;
pub use zoom_controls::*;
mod style_switcher;
@@ -11,8 +10,9 @@ mod panel;
mod panel_imp;
pub use panel::*;
-mod tile_header;
-pub use tile_header::*;
+mod panel_header;
+mod panel_header_imp;
+pub use panel_header::*;
mod split_paned;
@@ -20,9 +20,16 @@ mod panel_grid;
mod panel_grid_imp;
pub use panel_grid::*;
-pub fn signal_accumulator_propagation(_hint: &SignalInvocationHint, return_accu: &mut glib::Value, handler_return: &glib::Value) -> bool {
- let signal_propagate = glib::Propagation::from(handler_return.get::().unwrap_or(true));
+mod bin;
+mod bin_imp;
+pub use bin::*;
- *return_accu = handler_return.clone();
- signal_propagate.into()
-}
+mod pack_box;
+mod pack_box_imp;
+pub use pack_box::*;
+
+mod fading_label;
+mod fading_label_imp;
+pub use fading_label::*;
+
+pub mod utils;
diff --git a/src/twl/pack_box.rs b/src/twl/pack_box.rs
new file mode 100644
index 0000000..669051f
--- /dev/null
+++ b/src/twl/pack_box.rs
@@ -0,0 +1,28 @@
+use glib::{prelude::*, subclass::prelude::*};
+
+use super::{pack_box_imp as imp, utils::TwlWidgetExt};
+
+glib::wrapper! {
+ pub struct PackBox(ObjectSubclass)
+ @extends gtk::Widget,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+}
+
+impl PackBox {
+ pub fn new() -> Self {
+ glib::Object::builder().build()
+ }
+
+ pub fn append(&self, child: &impl IsA) {
+ self.imp().append(child);
+ }
+
+ pub fn pack_start(&self, child: &impl IsA) {
+ self.imp().pack_start(child);
+ }
+
+ pub fn pack_end(&self, child: &impl IsA) {
+ self.imp().pack_end(child);
+ }
+}
+impl TwlWidgetExt for PackBox {}
diff --git a/src/twl/pack_box_imp.rs b/src/twl/pack_box_imp.rs
new file mode 100644
index 0000000..91f491a
--- /dev/null
+++ b/src/twl/pack_box_imp.rs
@@ -0,0 +1,137 @@
+use std::{cmp::Ordering, marker::PhantomData};
+
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use glib::{self, Properties};
+use gtk::graphene;
+
+use approx::abs_diff_eq;
+use num_traits as num;
+
+use super::utils::{twl_widget_compute_expand, Orthogonal, TwlWidgetExt};
+
+#[derive(Debug, Default, Properties)]
+#[properties(wrapper_type=super::PackBox)]
+pub struct PackBox {
+ container: gtk::Box,
+ start: gtk::Box,
+ center: gtk::Box,
+ end: gtk::Box,
+
+ #[property(get=Self::get_orientation, set=Self::set_orientation, construct, explicit_notify, builder(gtk::Orientation::Horizontal))]
+ orientation: PhantomData,
+}
+
+// impl Default for PackBox {
+// fn default() -> Self {
+// let orientation = gtk::Orientation::Horizontal;
+// let spacing = 6;
+// Self {
+// container: gtk::Box::new(orientation, spacing),
+// start: gtk::Box::new(orientation, spacing),
+// center: gtk::Box::new(orientation, spacing),
+// end: gtk::Box::new(orientation, spacing),
+
+// orientation: Default::default(),
+// }
+// }
+// }
+
+#[glib::object_subclass]
+impl ObjectSubclass for PackBox {
+ const NAME: &'static str = "TwlPackBox";
+ type Type = super::PackBox;
+ type ParentType = gtk::Widget;
+ type Interfaces = (gtk::Buildable, gtk::Orientable);
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.set_layout_manager_type::();
+ klass.set_css_name("pack_box");
+ }
+}
+
+#[glib::derived_properties]
+impl ObjectImpl for PackBox {
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ self.container.set_parent(self.obj().as_ref());
+ self.container.set_hexpand(true);
+ self.container.set_vexpand(true);
+
+ self.container.append(&self.start);
+ self.container.append(&self.center);
+ self.container.append(&self.end);
+
+ self.center.set_hexpand(true);
+ self.center.set_vexpand(true);
+ self.apply_orientation();
+ }
+
+ fn dispose(&self) {
+ self.container.unparent();
+ }
+}
+
+impl WidgetImpl for PackBox {
+ fn compute_expand(&self, hexpand: &mut bool, vexpand: &mut bool) {
+ twl_widget_compute_expand(&self.container, hexpand, vexpand);
+ }
+}
+
+impl OrientableImpl for PackBox {}
+
+impl BuildableImpl for PackBox {
+ fn add_child(&self, builder: >k::Builder, child: &glib::Object, type_: Option<&str>) {
+ match (child.downcast_ref::(), type_) {
+ (Some(widget), Some(wtype)) if wtype == "start" => self.pack_start(widget),
+ (Some(widget), Some(wtype)) if wtype == "end" => self.pack_end(widget),
+ (Some(widget), _) => self.append(widget),
+ (_, _) => self.parent_add_child(builder, child, type_),
+ }
+ }
+}
+
+impl PackBox {
+ fn set_orientation(&self, orientation: gtk::Orientation) {
+ if self.get_orientation() == orientation {
+ return;
+ }
+
+ self.container.set_orientation(orientation);
+
+ self.apply_orientation();
+ self.obj().notify_orientation();
+ }
+
+ fn get_orientation(&self) -> gtk::Orientation {
+ self.container.orientation()
+ }
+
+ fn apply_orientation(&self) {
+ let orientation = self.get_orientation();
+ let is_horizontal = orientation == gtk::Orientation::Horizontal;
+
+ self.start.set_orientation(orientation);
+ self.start.set_hexpand(!is_horizontal);
+ self.start.set_vexpand(is_horizontal);
+
+ self.center.set_orientation(orientation);
+
+ self.end.set_orientation(orientation);
+ self.end.set_hexpand(!is_horizontal);
+ self.end.set_vexpand(is_horizontal);
+ }
+
+ pub fn pack_start(&self, widget: &impl IsA) {
+ self.start.append(widget);
+ }
+
+ pub fn append(&self, widget: &impl IsA) {
+ self.center.append(widget);
+ }
+
+ pub fn pack_end(&self, widget: &impl IsA) {
+ self.end.append(widget);
+ }
+}
diff --git a/src/twl/panel.rs b/src/twl/panel.rs
index ac752fb..b4b6157 100644
--- a/src/twl/panel.rs
+++ b/src/twl/panel.rs
@@ -1,6 +1,6 @@
-use glib::{prelude::*, subclass::prelude::*};
+use glib::{closure_local, prelude::*, subclass::prelude::*};
-use super::panel_imp as imp;
+use super::{panel_imp as imp, utils::TwlWidgetExt};
glib::wrapper! {
pub struct Panel(ObjectSubclass)
@@ -9,8 +9,8 @@ glib::wrapper! {
}
impl Panel {
- pub fn new(child: &impl IsA) -> Self {
- glib::Object::builder().property("child", child).build()
+ pub fn new(content: &impl IsA) -> Self {
+ glib::Object::builder().property("content", content).build()
}
pub fn set_closing(&self, closing: bool) {
@@ -20,4 +20,10 @@ impl Panel {
pub fn closing(&self) -> bool {
self.imp().closing.get()
}
+
+ pub fn connect_close(&self, f: F) -> glib::SignalHandlerId {
+ self.connect_closure("close", false, closure_local!(move |obj: Self| { f(&obj) }))
+ }
}
+
+impl TwlWidgetExt for Panel {}
diff --git a/src/twl/panel_grid.rs b/src/twl/panel_grid.rs
index 3646392..6f7e707 100644
--- a/src/twl/panel_grid.rs
+++ b/src/twl/panel_grid.rs
@@ -1,7 +1,7 @@
use glib::{closure_local, prelude::*};
use gtk::subclass::prelude::*;
-use super::{panel_grid_imp as imp, Panel};
+use super::{panel_grid_imp as imp, utils::TwlWidgetExt, Panel};
glib::wrapper! {
pub struct PanelGrid(ObjectSubclass)
@@ -50,3 +50,5 @@ impl PanelGrid {
)
}
}
+
+impl TwlWidgetExt for PanelGrid {}
diff --git a/src/twl/panel_grid_imp.rs b/src/twl/panel_grid_imp.rs
index 0055099..58116c6 100644
--- a/src/twl/panel_grid_imp.rs
+++ b/src/twl/panel_grid_imp.rs
@@ -8,9 +8,10 @@ use glib::{clone, subclass::Signal, Properties};
use once_cell::sync::Lazy;
use tracing::*;
-use crate::twl::signal_accumulator_propagation;
+use crate::twl::utils::signal_accumulator_propagation;
use super::split_paned::SplitPaned;
+use super::utils::TwlWidgetExt;
use super::Panel;
#[derive(Debug, Default, Properties)]
@@ -39,6 +40,7 @@ impl ObjectSubclass for PanelGrid {
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::();
+ klass.set_css_name("panel_grid");
}
}
@@ -124,29 +126,26 @@ impl PanelGrid {
where
T: IsA + ObjectType,
{
- self.get_all_inner(&self.inner).into_iter().collect()
+ self.get_all_inner(&self.inner.upcast_ref()).into_iter().collect()
}
- fn get_all_inner(&self, root: &impl IsA) -> HashSet
+ fn get_all_inner(&self, root: >k::Widget) -> HashSet
where
T: IsA + ObjectType,
{
let mut elems = HashSet::new();
- if let Ok(relem) = root.as_ref().clone().downcast() {
+ if let Ok(relem) = root.clone().downcast() {
elems.insert(relem);
}
- let mut sibling = root.first_child();
- while let Some(widget) = sibling {
- if let Ok(elem) = widget.clone().downcast() {
+ for child in root.iter_children() {
+ if let Ok(elem) = child.clone().downcast() {
elems.insert(elem);
}
- let child_elems = self.get_all_inner(&widget);
+ let child_elems = self.get_all_inner(&child);
elems.extend(child_elems);
-
- sibling = widget.next_sibling();
}
elems
@@ -161,6 +160,11 @@ impl PanelGrid {
this.on_panel_focus(&panel);
}
}));
+
+ panel.connect_close(clone!(@weak self as this => move |p| {
+ this.close_panel(p);
+ }));
+
panel
}
@@ -175,14 +179,16 @@ impl PanelGrid {
}
pub fn split(&self, child: &impl IsA, orientation: Option) -> Panel {
- let root_panel = self.selected_panel.borrow().clone().or_else(|| self.get_all::().first().cloned());
- debug!("root panel {:?}", root_panel);
+ let active_panel = self.selected_panel.borrow().clone().or_else(|| self.get_all::().first().cloned());
+ debug!("active panel {:?}", active_panel);
- if let Some(root_panel) = root_panel {
+ if let Some(active_panel) = active_panel {
+ debug!("split: split active panel");
let panel = self.create_panel(child);
- self.split_panel(&root_panel, &panel, orientation);
+ self.split_panel(&active_panel, &panel, orientation);
panel
} else {
+ debug!("split: set initial child");
self.set_initial_child(child)
}
}
@@ -190,6 +196,7 @@ impl PanelGrid {
fn split_panel(&self, panel: &Panel, new_panel: &Panel, orientation: Option) {
let new_paned = gtk::Paned::new(orientation.unwrap_or_else(|| self.preferred_orientation(panel)));
new_paned.set_wide_handle(self.wide_handle.get());
+ debug!("panel {:?} parent {:?}", panel, panel.parent());
match panel.parent().and_downcast::() {
// if the widget does not belong to a paned, it has to be the root
diff --git a/src/twl/panel_header.rs b/src/twl/panel_header.rs
new file mode 100644
index 0000000..b8d7033
--- /dev/null
+++ b/src/twl/panel_header.rs
@@ -0,0 +1,19 @@
+use std::path::PathBuf;
+
+use super::{panel_header_imp as imp, utils::TwlWidgetExt, Panel};
+use glib::closure_local;
+use gtk::prelude::*;
+
+glib::wrapper! {
+ pub struct PanelHeader(ObjectSubclass)
+ @extends gtk::Widget,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
+}
+
+impl PanelHeader {
+ pub fn new(panel: &impl IsA) -> Self {
+ glib::Object::builder().property("panel", panel).build()
+ }
+}
+
+impl TwlWidgetExt for PanelHeader {}
diff --git a/src/twl/panel_header.ui b/src/twl/panel_header.ui
new file mode 100644
index 0000000..74d8e92
--- /dev/null
+++ b/src/twl/panel_header.ui
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
diff --git a/src/twl/panel_header_imp.rs b/src/twl/panel_header_imp.rs
new file mode 100644
index 0000000..585bbd1
--- /dev/null
+++ b/src/twl/panel_header_imp.rs
@@ -0,0 +1,140 @@
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use glib::subclass::Signal;
+use glib::{self, clone, Properties, WeakRef};
+use gtk::{template_callbacks, CompositeTemplate};
+use itertools::Itertools;
+use once_cell::sync::Lazy;
+use std::cell::{Cell, RefCell};
+use std::marker::PhantomData;
+
+use super::utils::TwlWidgetExt;
+use super::{Bin, FadingLabel, PackBox, Panel};
+
+// #[derive(Debug, Default)]
+// pub struct Header {}
+
+// #[glib::object_interface]
+// unsafe impl ObjectInterface for Header {
+// const NAME: &'static str = "TwlHeader";
+// }
+
+#[derive(Debug, CompositeTemplate, Properties)]
+#[template(resource = "/io/github/vhdirk/Twl/gtk/panel_header.ui")]
+#[properties(wrapper_type=super::PanelHeader)]
+pub struct PanelHeader {
+ #[property(get, set=Self::set_title, construct, nullable)]
+ title: RefCell