[fractal/fractal-next] components: Create ActionButton



commit c7de7eb431871c75a02a6c9d58548af3afcaef6e
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Tue Feb 8 12:22:54 2022 +0100

    components: Create ActionButton

 data/resources/resources.gresource.xml        |   1 +
 data/resources/style.css                      |   6 +-
 data/resources/ui/components-action-button.ui | 138 ++++++++++++++
 src/components/action_button.rs               | 247 ++++++++++++++++++++++++++
 src/components/mod.rs                         |   2 +
 5 files changed, 393 insertions(+), 1 deletion(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 9ccb23230..813d0a753 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -16,6 +16,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="account-settings.ui">ui/account-settings.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="add-account-row.ui">ui/add-account-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="avatar-with-selection.ui">ui/avatar-with-selection.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="components-action-button.ui">ui/components-action-button.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-audio-player.ui">ui/components-audio-player.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-auth-dialog.ui">ui/components-auth-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-avatar.ui">ui/components-avatar.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 38dc3fa17..5dab47a24 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -31,6 +31,11 @@ headerbar .suggested-action {
   min-width: 70px;
 }
 
+button.opaque.success {
+  color: @success_fg_color;
+  background-color: @success_bg_color;
+}
+
 
 /* Components */
 
@@ -78,7 +83,6 @@ headerbar .suggested-action {
   background-color: @yellow_5;
 }
 
-
 /* Login */
 
 login {
diff --git a/data/resources/ui/components-action-button.ui b/data/resources/ui/components-action-button.ui
new file mode 100644
index 000000000..62bb5096e
--- /dev/null
+++ b/data/resources/ui/components-action-button.ui
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ComponentsActionButton" parent="AdwBin">
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="transition-type">crossfade</property>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">default</property>
+            <property name="child">
+              <object class="GtkButton" id="button_default">
+                <style>
+                  <class name="circular"/>
+                </style>
+                <property name="valign">center</property>
+                <property name="icon-name" bind-source="ComponentsActionButton" bind-property="icon-name" 
bind-flags="sync-create"/>
+                <property name="action-name" bind-source="ComponentsActionButton" 
bind-property="action-name" bind-flags="sync-create"/>
+                <property name="action-target" bind-source="ComponentsActionButton" 
bind-property="action-target" bind-flags="sync-create"/>
+                <signal name="clicked" handler="button_clicked" swapped="true"/>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">confirm</property>
+            <property name="child">
+              <object class="GtkButton">
+                <style>
+                  <class name="opaque"/>
+                  <class name="circular"/>
+                  <class name="suggested-action"/>
+                </style>
+                <property name="valign">center</property>
+                <property name="icon-name">emblem-ok-symbolic</property>
+                <property name="action-name" bind-source="ComponentsActionButton" 
bind-property="action-name" bind-flags="sync-create"/>
+                <property name="action-target" bind-source="ComponentsActionButton" 
bind-property="action-target" bind-flags="sync-create"/>
+                <signal name="clicked" handler="button_clicked" swapped="true"/>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">retry</property>
+            <property name="child">
+              <object class="GtkButton">
+                <style>
+                  <class name="opaque"/>
+                  <class name="circular"/>
+                  <class name="suggested-action"/>
+                </style>
+                <property name="valign">center</property>
+                <property name="icon-name">view-refresh-symbolic</property>
+                <property name="action-name" bind-source="ComponentsActionButton" 
bind-property="action-name" bind-flags="sync-create"/>
+                <property name="action-target" bind-source="ComponentsActionButton" 
bind-property="action-target" bind-flags="sync-create"/>
+                <signal name="clicked" handler="button_clicked" swapped="true"/>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">loading</property>
+            <property name="child">
+              <object class="GtkButton">
+                <style>
+                  <class name="circular"/>
+                </style>
+                <property name="focusable">false</property>
+                <property name="can-target">false</property>
+                <property name="valign">center</property>
+                <child>
+                  <object class="GtkSpinner">
+                    <property name="spinning">true</property>
+                    <property name="valign">center</property>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">success</property>
+            <property name="child">
+              <object class="GtkButton">
+                <style>
+                  <class name="opaque"/>
+                  <class name="circular"/>
+                  <class name="success"/>
+                </style>
+                <property name="focusable">false</property>
+                <property name="can-target">false</property>
+                <property name="valign">center</property>
+                <property name="icon-name">emblem-ok-symbolic</property>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">warning</property>
+            <property name="child">
+              <object class="GtkButton" id="button_warning">
+                <style>
+                  <class name="circular"/>
+                  <class name="warning"/>
+                </style>
+                <property name="focusable">false</property>
+                <property name="can-target">false</property>
+                <property name="valign">center</property>
+                <property name="icon-name">dialog-warning-symbolic</property>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">error</property>
+            <property name="child">
+              <object class="GtkButton">
+                <style>
+                  <class name="circular"/>
+                  <class name="error"/>
+                </style>
+                <property name="focusable">false</property>
+                <property name="can-target">false</property>
+                <property name="valign">center</property>
+                <property name="icon-name">dialog-error-symbolic</property>
+              </object>
+            </property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/components/action_button.rs b/src/components/action_button.rs
new file mode 100644
index 000000000..256e75cda
--- /dev/null
+++ b/src/components/action_button.rs
@@ -0,0 +1,247 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
+#[repr(u32)]
+#[enum_type(name = "ActionState")]
+pub enum ActionState {
+    Default = 0,
+    Confirm = 1,
+    Retry = 2,
+    Loading = 3,
+    Success = 4,
+    Warning = 5,
+    Error = 6,
+}
+
+impl Default for ActionState {
+    fn default() -> Self {
+        Self::Default
+    }
+}
+
+impl AsRef<str> for ActionState {
+    fn as_ref(&self) -> &str {
+        match self {
+            ActionState::Default => "default",
+            ActionState::Confirm => "confirm",
+            ActionState::Retry => "retry",
+            ActionState::Loading => "loading",
+            ActionState::Success => "success",
+            ActionState::Warning => "warning",
+            ActionState::Error => "error",
+        }
+    }
+}
+
+mod imp {
+    use std::cell::{Cell, RefCell};
+
+    use glib::subclass::{InitializingObject, Signal};
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/components-action-button.ui")]
+    pub struct ActionButton {
+        /// The icon used in the default state.
+        pub icon_name: RefCell<String>,
+        /// The extra classes applied to the button in the default state.
+        pub extra_classes: RefCell<Vec<String>>,
+        /// The action emitted by the button.
+        pub action_name: RefCell<Option<glib::GString>>,
+        /// The target value of the action of the button.
+        pub action_target_value: RefCell<Option<glib::Variant>>,
+        /// The state of the button.
+        pub state: Cell<ActionState>,
+        #[template_child]
+        pub stack: TemplateChild<gtk::Stack>,
+        #[template_child]
+        pub button_default: TemplateChild<gtk::Button>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for ActionButton {
+        const NAME: &'static str = "ComponentsActionButton";
+        type Type = super::ActionButton;
+        type ParentType = adw::Bin;
+        type Interfaces = (gtk::Actionable,);
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+            Self::Type::bind_template_callbacks(klass);
+            klass.set_css_name("action-button");
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for ActionButton {
+        fn signals() -> &'static [Signal] {
+            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+                vec![Signal::builder("clicked", &[], <()>::static_type().into()).build()]
+            });
+            SIGNALS.as_ref()
+        }
+
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpecString::new(
+                        "icon-name",
+                        "Icon Name",
+                        "The icon used in the default state",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecEnum::new(
+                        "state",
+                        "State",
+                        "The state of the button",
+                        ActionState::static_type(),
+                        ActionState::default() as i32,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecOverride::for_interface::<gtk::Actionable>("action-name"),
+                    glib::ParamSpecOverride::for_interface::<gtk::Actionable>("action-target"),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "icon-name" => obj.set_icon_name(value.get().unwrap()),
+                "state" => obj.set_state(value.get().unwrap()),
+                "action-name" => obj.set_action_name(value.get().unwrap()),
+                "action-target" => obj.set_action_target_value(
+                    value.get::<Option<glib::Variant>>().unwrap().as_ref(),
+                ),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "icon-name" => obj.icon_name().to_value(),
+                "state" => obj.state().to_value(),
+                "action-name" => obj.action_name().to_value(),
+                "action-target" => obj.action_target_value().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl WidgetImpl for ActionButton {}
+    impl BinImpl for ActionButton {}
+
+    impl ActionableImpl for ActionButton {
+        fn action_name(&self, _obj: &Self::Type) -> Option<glib::GString> {
+            self.action_name.borrow().clone()
+        }
+
+        fn action_target_value(&self, _obj: &Self::Type) -> Option<glib::Variant> {
+            self.action_target_value.borrow().clone()
+        }
+
+        fn set_action_name(&self, _obj: &Self::Type, name: Option<&str>) {
+            self.action_name.replace(name.map(Into::into));
+        }
+
+        fn set_action_target_value(&self, _obj: &Self::Type, value: Option<&glib::Variant>) {
+            self.action_target_value
+                .replace(value.map(ToOwned::to_owned));
+        }
+    }
+}
+
+glib::wrapper! {
+    /// A button to emit an action and handle its different states.
+    pub struct ActionButton(ObjectSubclass<imp::ActionButton>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Actionable, gtk::Accessible;
+}
+
+#[gtk::template_callbacks]
+impl ActionButton {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create ActionButton")
+    }
+
+    pub fn icon_name(&self) -> String {
+        self.imp().icon_name.borrow().clone()
+    }
+
+    pub fn set_icon_name(&self, icon_name: &str) {
+        if self.icon_name() == icon_name {
+            return;
+        }
+
+        self.imp().icon_name.replace(icon_name.to_owned());
+        self.notify("icon-name");
+    }
+
+    pub fn extra_classes(&self) -> Vec<String> {
+        self.imp().extra_classes.borrow().clone()
+    }
+
+    pub fn set_extra_classes(&self, classes: &[&str]) {
+        let priv_ = self.imp();
+        for class in priv_.extra_classes.borrow_mut().drain(..) {
+            priv_.button_default.remove_css_class(&class);
+        }
+
+        for class in classes.iter() {
+            priv_.button_default.add_css_class(class);
+        }
+
+        self.imp()
+            .extra_classes
+            .replace(classes.iter().map(ToString::to_string).collect());
+    }
+
+    pub fn state(&self) -> ActionState {
+        self.imp().state.get()
+    }
+
+    pub fn set_state(&self, state: ActionState) {
+        if self.state() == state {
+            return;
+        }
+
+        let priv_ = self.imp();
+        priv_.stack.set_visible_child_name(state.as_ref());
+        priv_.state.replace(state);
+        self.notify("state");
+    }
+
+    pub fn connect_clicked<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "clicked",
+            true,
+            closure_local!(move |obj: Self| {
+                f(&obj);
+            }),
+        )
+    }
+
+    #[template_callback]
+    fn button_clicked(&self) {
+        self.emit_by_name::<()>("clicked", &[]);
+    }
+}
+
+impl Default for ActionButton {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 53eb2f89b..86e47298a 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,3 +1,4 @@
+mod action_button;
 mod audio_player;
 mod auth_dialog;
 mod avatar;
@@ -16,6 +17,7 @@ mod video_player;
 mod video_player_renderer;
 
 pub use self::{
+    action_button::{ActionButton, ActionState},
     audio_player::AudioPlayer,
     auth_dialog::{AuthData, AuthDialog, AuthError},
     avatar::Avatar,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]