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 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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