diff --git a/data/resources/assets/preview-dark.svg b/data/resources/assets/preview-dark.svg
new file mode 100644
index 000000000..50493f330
--- /dev/null
+++ b/data/resources/assets/preview-dark.svg
@@ -0,0 +1,330 @@
+
+
diff --git a/data/resources/assets/preview-light.svg b/data/resources/assets/preview-light.svg
new file mode 100644
index 000000000..6b95ed8cf
--- /dev/null
+++ b/data/resources/assets/preview-light.svg
@@ -0,0 +1,316 @@
+
+
diff --git a/data/resources/assets/preview-system.svg b/data/resources/assets/preview-system.svg
new file mode 100644
index 000000000..6bee33816
--- /dev/null
+++ b/data/resources/assets/preview-system.svg
@@ -0,0 +1,493 @@
+
+
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 902975900..73725f2c9 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -1,6 +1,10 @@
+ assets/preview-dark.svg
+ assets/preview-light.svg
+ assets/preview-system.svg
+
icons/scalable/actions/big-x-symbolic.svg
icons/scalable/actions/clear-symbolic.svg
icons/scalable/actions/done-symbolic.svg
diff --git a/data/resources/style.css b/data/resources/style.css
index aa82ab940..09c00ddbd 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -430,3 +430,20 @@ splitbutton.small-pill:dir(ltr) > menubutton > button {
border-top-right-radius: 18px;
border-bottom-right-radius: 18px;
}
+
+styleselectionrow stylevariantpreview .wallpaper {
+ border-radius: 6px;
+ box-shadow: 0 0 9px 1px rgba(0,0,0,.2);
+}
+
+styleselectionrow button {
+ padding: 0;
+ margin: 0;
+ border: 3px solid transparent;
+ border-radius: 11px;
+ background: transparent;
+}
+
+styleselectionrow button:checked {
+ border-color: @theme_selected_bg_color;
+}
diff --git a/src/application.rs b/src/application.rs
index 66c7c076c..8db863c14 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -65,7 +65,7 @@ mod imp {
obj.setup_gactions();
obj.setup_accels();
- obj.load_color_scheme();
+ obj.setup_theming();
}
}
@@ -141,21 +141,40 @@ impl Application {
self.add_action(&action_new_login_test_server);
}
+ fn setup_theming(&self) {
+ let style_manager = adw::StyleManager::default();
+
+ gio::Settings::new(config::APP_ID)
+ .bind("color-scheme", &style_manager, "color-scheme")
+ .get()
+ .set_mapping(|value, _| Some(color_scheme_to_str(value.get().unwrap()).to_variant()))
+ .set()
+ .mapping(|variant, _| Some(str_to_color_scheme(variant.str().unwrap()).to_value()))
+ .build();
+
+ let action = gio::SimpleAction::new_stateful(
+ "style-variant",
+ Some(glib::VariantTy::STRING),
+ &color_scheme_to_str(style_manager.color_scheme()).to_variant(),
+ );
+ action.connect_activate(clone!(@weak self as obj => move |_, param| {
+ adw::StyleManager::default()
+ .set_color_scheme(str_to_color_scheme(param.unwrap().str().unwrap()));
+ }));
+ self.add_action(&action);
+
+ adw::StyleManager::default().connect_color_scheme_notify(
+ clone!(@weak action => move |style_manager| {
+ action.set_state(&color_scheme_to_str(style_manager.color_scheme()).to_variant());
+ }),
+ );
+ }
+
// Sets up keyboard shortcuts
fn setup_accels(&self) {
self.set_accels_for_action("app.quit", &["q"]);
}
- fn load_color_scheme(&self) {
- let style_manager = adw::StyleManager::default();
- let settings = gio::Settings::new(config::APP_ID);
- match settings.string("color-scheme").as_ref() {
- "light" => style_manager.set_color_scheme(adw::ColorScheme::ForceLight),
- "dark" => style_manager.set_color_scheme(adw::ColorScheme::ForceDark),
- _ => style_manager.set_color_scheme(adw::ColorScheme::PreferLight),
- }
- }
-
fn show_about_dialog(&self) {
let about = adw::AboutWindow::builder()
.transient_for(&self.main_window())
@@ -207,3 +226,19 @@ impl Application {
about.present();
}
}
+
+fn color_scheme_to_str(scheme: adw::ColorScheme) -> &'static str {
+ match scheme {
+ adw::ColorScheme::ForceDark | adw::ColorScheme::PreferDark => "dark",
+ adw::ColorScheme::ForceLight | adw::ColorScheme::PreferLight => "light",
+ _ => "default",
+ }
+}
+
+fn str_to_color_scheme(scheme: &str) -> adw::ColorScheme {
+ match scheme {
+ "light" => adw::ColorScheme::ForceLight,
+ "dark" => adw::ColorScheme::ForceDark,
+ _ => adw::ColorScheme::Default,
+ }
+}
diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs
index d564241b0..fef49d33b 100644
--- a/src/ui/components/mod.rs
+++ b/src/ui/components/mod.rs
@@ -9,6 +9,8 @@ mod message_entry;
mod phone_number_input;
mod snow;
mod sticker;
+mod style_selection_row;
+mod style_variant_preview;
pub(crate) use self::animated_bin::AnimatedBin;
pub(crate) use self::avatar::Avatar;
@@ -21,3 +23,5 @@ pub(crate) use self::message_entry::MessageEntry;
pub(crate) use self::phone_number_input::PhoneNumberInput;
pub(crate) use self::snow::Snow;
pub(crate) use self::sticker::Sticker;
+pub(crate) use self::style_selection_row::StyleSelectionRow;
+pub(crate) use self::style_variant_preview::StyleVariantPreview;
diff --git a/src/ui/components/style_selection_row.blp b/src/ui/components/style_selection_row.blp
new file mode 100644
index 000000000..b5ced4a92
--- /dev/null
+++ b/src/ui/components/style_selection_row.blp
@@ -0,0 +1,72 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $StyleSelectionRow : Adw.PreferencesRow {
+ activatable: false;
+ title: _("Color Scheme");
+
+ Box {
+ halign: center;
+ margin-top: 15;
+ margin-end: 3;
+ margin-bottom: 15;
+ margin-start: 3;
+ spacing: 12;
+
+ Box {
+ orientation: vertical;
+ spacing: 6;
+
+ ToggleButton {
+ action-name: "app.style-variant";
+ action-target: "'default'";
+
+ $StyleVariantPreview {
+ color-scheme: default;
+ }
+ }
+
+ Label {
+ label: _("Follow System Colors");
+ wrap: true;
+ wrap-mode: word_char;
+ }
+ }
+
+ Box {
+ orientation: vertical;
+ spacing: 6;
+
+ ToggleButton {
+ action-name: "app.style-variant";
+ action-target: "'light'";
+
+ $StyleVariantPreview {
+ color-scheme: prefer-light;
+ }
+ }
+
+ Label {
+ label: _("Light");
+ }
+ }
+
+ Box {
+ orientation: vertical;
+ spacing: 6;
+
+ ToggleButton {
+ action-name: "app.style-variant";
+ action-target: "'dark'";
+
+ $StyleVariantPreview {
+ color-scheme: prefer-dark;
+ }
+ }
+
+ Label {
+ label: _("Dark");
+ }
+ }
+ }
+}
diff --git a/src/ui/components/style_selection_row.rs b/src/ui/components/style_selection_row.rs
new file mode 100644
index 000000000..f53489103
--- /dev/null
+++ b/src/ui/components/style_selection_row.rs
@@ -0,0 +1,38 @@
+use adw::subclass::prelude::*;
+use gtk::glib;
+use gtk::CompositeTemplate;
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/app/drey/paper-plane/ui/components/style_selection_row.ui")]
+ pub(crate) struct StyleSelectionRow;
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for StyleSelectionRow {
+ const NAME: &'static str = "StyleSelectionRow";
+ type Type = super::StyleSelectionRow;
+ type ParentType = adw::PreferencesRow;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+ klass.set_css_name("styleselectionrow");
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for StyleSelectionRow {}
+ impl WidgetImpl for StyleSelectionRow {}
+ impl ListBoxRowImpl for StyleSelectionRow {}
+ impl PreferencesRowImpl for StyleSelectionRow {}
+}
+
+glib::wrapper! {
+ pub(crate) struct StyleSelectionRow(ObjectSubclass)
+ @extends gtk::Widget,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Actionable;
+}
diff --git a/src/ui/components/style_variant_preview.blp b/src/ui/components/style_variant_preview.blp
new file mode 100644
index 000000000..6ade9ad80
--- /dev/null
+++ b/src/ui/components/style_variant_preview.blp
@@ -0,0 +1,23 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $StyleVariantPreview {
+ layout-manager: BinLayout {};
+
+ margin-top: 2;
+ margin-end: 2;
+ margin-bottom: 2;
+ margin-start: 2;
+
+ Adw.Bin {
+ styles ["wallpaper"]
+
+ overflow: hidden;
+
+ Picture picture {
+ content-fit: fill;
+ width-request: 164;
+ height-request: 90;
+ }
+ }
+}
diff --git a/src/ui/components/style_variant_preview.rs b/src/ui/components/style_variant_preview.rs
new file mode 100644
index 000000000..0b442862f
--- /dev/null
+++ b/src/ui/components/style_variant_preview.rs
@@ -0,0 +1,92 @@
+use std::cell::Cell;
+
+use glib::Properties;
+use gtk::gdk;
+use gtk::glib;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::CompositeTemplate;
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Properties, CompositeTemplate)]
+ #[properties(wrapper_type = super::StyleVariantPreview)]
+ #[template(resource = "/app/drey/paper-plane/ui/components/style_variant_preview.ui")]
+ pub(crate) struct StyleVariantPreview {
+ #[property(get, set, builder(adw::ColorScheme::Default))]
+ pub(super) color_scheme: Cell,
+ #[template_child]
+ pub(super) picture: TemplateChild,
+ }
+
+ impl Default for StyleVariantPreview {
+ fn default() -> Self {
+ Self {
+ color_scheme: Cell::new(adw::ColorScheme::Default),
+ picture: Default::default(),
+ }
+ }
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for StyleVariantPreview {
+ const NAME: &'static str = "StyleVariantPreview";
+ type Type = super::StyleVariantPreview;
+ type ParentType = gtk::Widget;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+ klass.set_css_name("stylevariantpreview");
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for StyleVariantPreview {
+ fn properties() -> &'static [glib::ParamSpec] {
+ Self::derived_properties()
+ }
+
+ fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
+ Self::derived_set_property(self, id, value, pspec)
+ }
+
+ fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ Self::derived_property(self, id, pspec)
+ }
+
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ self.obj().connect_color_scheme_notify(|obj| {
+ let texture = gdk::Texture::from_resource(match obj.color_scheme() {
+ adw::ColorScheme::PreferLight | adw::ColorScheme::ForceLight => {
+ "/app/drey/paper-plane/assets/preview-light.svg"
+ }
+ adw::ColorScheme::PreferDark | adw::ColorScheme::ForceDark => {
+ "/app/drey/paper-plane/assets/preview-dark.svg"
+ }
+ _ => "/app/drey/paper-plane/assets/preview-system.svg",
+ });
+ obj.imp().picture.set_paintable(Some(&texture));
+ });
+ }
+
+ fn dispose(&self) {
+ let mut child = self.obj().first_child();
+ while let Some(child_) = child {
+ child = child_.next_sibling();
+ child_.unparent();
+ }
+ }
+ }
+
+ impl WidgetImpl for StyleVariantPreview {}
+}
+
+glib::wrapper! {
+ pub(crate) struct StyleVariantPreview(ObjectSubclass) @extends gtk::Widget;
+}
diff --git a/src/ui/meson.build b/src/ui/meson.build
index 672b7e777..357ac74af 100644
--- a/src/ui/meson.build
+++ b/src/ui/meson.build
@@ -12,6 +12,8 @@ blueprints = custom_target('blueprints',
'components/map.blp',
'components/message_entry.blp',
'components/phone_number_input.blp',
+ 'components/style_selection_row.blp',
+ 'components/style_variant_preview.blp',
'login/code.blp',
'login/mod.blp',
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 917485275..2bd451969 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -20,6 +20,8 @@ pub(crate) use self::components::MessageEntry;
pub(crate) use self::components::PhoneNumberInput;
pub(crate) use self::components::Snow;
pub(crate) use self::components::Sticker;
+pub(crate) use self::components::StyleSelectionRow;
+pub(crate) use self::components::StyleVariantPreview;
pub(crate) use self::login::Code as LoginCode;
pub(crate) use self::login::Login;
pub(crate) use self::login::OtherDevice as LoginOtherDevice;
@@ -133,5 +135,7 @@ pub(crate) fn init() {
SidebarSearchSectionType::static_type();
Snow::static_type();
Sticker::static_type();
+ StyleSelectionRow::static_type();
+ StyleVariantPreview::static_type();
Window::static_type();
}
diff --git a/src/ui/session/preferences_window.blp b/src/ui/session/preferences_window.blp
index 069110646..ffb478e33 100644
--- a/src/ui/session/preferences_window.blp
+++ b/src/ui/session/preferences_window.blp
@@ -4,25 +4,9 @@ using Adw 1;
template $PaplPreferencesWindow : Adw.PreferencesWindow {
Adw.PreferencesPage {
Adw.PreferencesGroup {
- title: _("Color Scheme");
+ title: _("Interface");
- Adw.ActionRow {
- title: _("Follow System Colors");
- activatable-widget: follow_system_colors_switch;
-
- Switch follow_system_colors_switch {
- valign: center;
- }
- }
-
- Adw.ActionRow {
- title: _("Dark Theme");
- activatable-widget: dark_theme_switch;
-
- Switch dark_theme_switch {
- valign: center;
- }
- }
+ $StyleSelectionRow { }
}
Adw.PreferencesGroup {
diff --git a/src/ui/session/preferences_window.rs b/src/ui/session/preferences_window.rs
index 1fce0ca15..84f32563f 100644
--- a/src/ui/session/preferences_window.rs
+++ b/src/ui/session/preferences_window.rs
@@ -4,12 +4,10 @@ use adw::prelude::*;
use adw::subclass::prelude::*;
use gettextrs::gettext;
use glib::clone;
-use gtk::gio;
use gtk::glib;
use gtk::CompositeTemplate;
use once_cell::sync::Lazy;
-use crate::config;
use crate::ui;
use crate::utils;
@@ -21,10 +19,6 @@ mod imp {
pub(crate) struct PreferencesWindow {
pub(super) session: OnceCell,
#[template_child]
- pub(super) follow_system_colors_switch: TemplateChild,
- #[template_child]
- pub(super) dark_theme_switch: TemplateChild,
- #[template_child]
pub(super) cache_size_label: TemplateChild,
}
@@ -82,20 +76,6 @@ mod imp {
let obj = self.obj();
- // If the system supports color schemes, load the 'Follow system colors'
- // switch state, otherwise make that switch insensitive
- let style_manager = adw::StyleManager::default();
- if style_manager.system_supports_color_schemes() {
- let settings = gio::Settings::new(config::APP_ID);
- let follow_system_colors = settings.string("color-scheme") == "default";
- self.follow_system_colors_switch
- .set_active(follow_system_colors);
- } else {
- self.follow_system_colors_switch.set_sensitive(false);
- }
-
- obj.setup_bindings();
-
utils::spawn(clone!(@weak obj => async move {
obj.calculate_cache_size().await;
}));
@@ -117,64 +97,14 @@ impl PreferencesWindow {
pub(crate) fn new(parent_window: Option<>k::Window>, session: &ui::Session) -> Self {
glib::Object::builder()
.property("transient-for", parent_window)
+ .property(
+ "application",
+ parent_window.and_then(gtk::Window::application),
+ )
.property("session", session)
.build()
}
- fn setup_bindings(&self) {
- let imp = self.imp();
-
- // 'Follow system colors' switch state handling
- imp.follow_system_colors_switch
- .connect_active_notify(|switch| {
- let style_manager = adw::StyleManager::default();
- let settings = gio::Settings::new(config::APP_ID);
- if switch.is_active() {
- // Prefer light theme unless the system prefers dark colors
- style_manager.set_color_scheme(adw::ColorScheme::PreferLight);
- settings.set_string("color-scheme", "default").unwrap();
- } else {
- // Set default state for the dark theme switch
- style_manager.set_color_scheme(adw::ColorScheme::ForceLight);
- settings.set_string("color-scheme", "light").unwrap();
- }
- });
-
- // 'Dark theme' switch state handling
- let follow_system_colors_switch = &*imp.follow_system_colors_switch;
- imp.dark_theme_switch.connect_active_notify(
- clone!(@weak follow_system_colors_switch => move |switch| {
- if !follow_system_colors_switch.is_active() {
- let style_manager = adw::StyleManager::default();
- let settings = gio::Settings::new(config::APP_ID);
- if switch.is_active() {
- // Dark mode
- style_manager.set_color_scheme(adw::ColorScheme::ForceDark);
- settings.set_string("color-scheme", "dark").unwrap();
- } else {
- // Light mode
- style_manager.set_color_scheme(adw::ColorScheme::ForceLight);
- settings.set_string("color-scheme", "light").unwrap();
- }
- }
- }),
- );
-
- // Make the 'Dark theme' switch insensitive if the 'Follow system colors'
- // switch is active
- imp.follow_system_colors_switch
- .bind_property("active", &*imp.dark_theme_switch, "sensitive")
- .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::INVERT_BOOLEAN)
- .build();
-
- // Have the 'Dark theme' switch state always updated with the dark state
- let style_manager = adw::StyleManager::default();
- style_manager
- .bind_property("dark", &*imp.dark_theme_switch, "active")
- .flags(glib::BindingFlags::SYNC_CREATE)
- .build();
- }
-
async fn calculate_cache_size(&self) {
let client_id = self.session().model().unwrap().client_().id();
match tdlib::functions::get_storage_statistics(0, client_id).await {
diff --git a/src/ui/ui-resources.gresource.xml b/src/ui/ui-resources.gresource.xml
index 8f162a6fd..be2b8154f 100644
--- a/src/ui/ui-resources.gresource.xml
+++ b/src/ui/ui-resources.gresource.xml
@@ -13,6 +13,8 @@
components/map.ui
components/message_entry.ui
components/phone_number_input.ui
+ components/style_selection_row.ui
+ components/style_variant_preview.ui
login/code.ui
login/mod.ui