fractal/components/
action_button.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::closure_local, CompositeTemplate};
3
4#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
5#[repr(u32)]
6#[enum_type(name = "ActionState")]
7pub enum ActionState {
8 #[default]
9 Default = 0,
10 Confirm = 1,
11 Retry = 2,
12 Loading = 3,
13 Success = 4,
14 Warning = 5,
15 Error = 6,
16}
17
18impl AsRef<str> for ActionState {
19 fn as_ref(&self) -> &str {
20 match self {
21 ActionState::Default => "default",
22 ActionState::Confirm => "confirm",
23 ActionState::Retry => "retry",
24 ActionState::Loading => "loading",
25 ActionState::Success => "success",
26 ActionState::Warning => "warning",
27 ActionState::Error => "error",
28 }
29 }
30}
31
32mod imp {
33 use std::{
34 cell::{Cell, RefCell},
35 marker::PhantomData,
36 sync::LazyLock,
37 };
38
39 use glib::subclass::{InitializingObject, Signal};
40
41 use super::*;
42
43 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
44 #[template(resource = "/org/gnome/Fractal/ui/components/action_button.ui")]
45 #[properties(wrapper_type = super::ActionButton)]
46 pub struct ActionButton {
47 #[template_child]
48 stack: TemplateChild<gtk::Stack>,
49 #[template_child]
50 button_default: TemplateChild<gtk::Button>,
51 #[property(get, set = Self::set_icon_name, explicit_notify)]
53 icon_name: RefCell<String>,
54 extra_classes: RefCell<Vec<&'static str>>,
56 #[property(get = Self::action_name, set = Self::set_action_name, override_interface = gtk::Actionable)]
58 action_name: RefCell<Option<glib::GString>>,
59 #[property(get = Self::action_target_value, set = Self::set_action_target, override_interface = gtk::Actionable)]
61 action_target: RefCell<Option<glib::Variant>>,
62 #[property(get, set = Self::set_state, explicit_notify, builder(ActionState::default()))]
64 state: Cell<ActionState>,
65 #[property(set = Self::set_default_state_tooltip_text)]
67 default_state_tooltip_text: PhantomData<Option<String>>,
68 }
69
70 #[glib::object_subclass]
71 impl ObjectSubclass for ActionButton {
72 const NAME: &'static str = "ActionButton";
73 type Type = super::ActionButton;
74 type ParentType = adw::Bin;
75 type Interfaces = (gtk::Actionable,);
76
77 fn class_init(klass: &mut Self::Class) {
78 Self::bind_template(klass);
79 Self::bind_template_callbacks(klass);
80
81 klass.set_css_name("action-button");
82 }
83
84 fn instance_init(obj: &InitializingObject<Self>) {
85 obj.init_template();
86 }
87 }
88
89 #[glib::derived_properties]
90 impl ObjectImpl for ActionButton {
91 fn signals() -> &'static [Signal] {
92 static SIGNALS: LazyLock<Vec<Signal>> =
93 LazyLock::new(|| vec![Signal::builder("clicked").build()]);
94 SIGNALS.as_ref()
95 }
96 }
97
98 impl WidgetImpl for ActionButton {}
99 impl BinImpl for ActionButton {}
100
101 impl ActionableImpl for ActionButton {
102 fn action_name(&self) -> Option<glib::GString> {
103 self.action_name.borrow().clone()
104 }
105
106 fn action_target_value(&self) -> Option<glib::Variant> {
107 self.action_target.borrow().clone()
108 }
109
110 fn set_action_name(&self, name: Option<&str>) {
111 self.action_name.replace(name.map(Into::into));
112 }
113
114 fn set_action_target_value(&self, value: Option<&glib::Variant>) {
115 self.set_action_target(value.cloned());
116 }
117 }
118
119 #[gtk::template_callbacks]
120 impl ActionButton {
121 fn set_icon_name(&self, icon_name: &str) {
123 if self.icon_name.borrow().as_str() == icon_name {
124 return;
125 }
126
127 self.icon_name.replace(icon_name.to_owned());
128 self.obj().notify_icon_name();
129 }
130
131 pub(super) fn set_extra_classes(&self, classes: &[&'static str]) {
134 let mut extra_classes = self.extra_classes.borrow_mut();
135
136 if *extra_classes == classes {
137 return;
139 }
140
141 for class in extra_classes.drain(..) {
142 self.button_default.remove_css_class(class);
143 }
144
145 for class in classes {
146 self.button_default.add_css_class(class);
147 }
148
149 extra_classes.extend(classes);
150 }
151
152 fn set_state(&self, state: ActionState) {
154 if self.state.get() == state {
155 return;
156 }
157
158 self.stack.set_visible_child_name(state.as_ref());
159 self.state.replace(state);
160 self.obj().notify_state();
161 }
162
163 fn set_action_target(&self, value: Option<glib::Variant>) {
165 self.action_target.replace(value);
166 }
167
168 fn set_default_state_tooltip_text(&self, text: Option<&str>) {
170 self.button_default.set_tooltip_text(text);
171 }
172
173 #[template_callback]
174 fn button_clicked(&self) {
175 self.obj().emit_by_name::<()>("clicked", &[]);
176 }
177 }
178}
179
180glib::wrapper! {
181 pub struct ActionButton(ObjectSubclass<imp::ActionButton>)
183 @extends gtk::Widget, adw::Bin, @implements gtk::Actionable, gtk::Accessible;
184}
185
186#[gtk::template_callbacks]
187impl ActionButton {
188 pub fn new() -> Self {
189 glib::Object::new()
190 }
191
192 pub(crate) fn set_extra_classes(&self, classes: &[&'static str]) {
194 self.imp().set_extra_classes(classes);
195 }
196
197 pub fn connect_clicked<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
199 self.connect_closure(
200 "clicked",
201 true,
202 closure_local!(move |obj: Self| {
203 f(&obj);
204 }),
205 )
206 }
207}