[fractal/fractal-next] components: Create ActionButton
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] components: Create ActionButton
- Date: Fri, 25 Feb 2022 09:48:44 +0000 (UTC)
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]