relm4_macros/lib.rs
1#![feature(doc_cfg)]
2//! A collection of macros for gtk-rs, Relm4 and Rust in general.
3//!
4//! Docs of related crates:
5//! [relm4](https://docs.rs/relm4)
6//! | [relm4-macros](https://docs.rs/relm4_macros)
7//! | [relm4-components](https://docs.rs/relm4_components)
8//! | [relm4-css](https://docs.rs/relm4-css)
9//! | [gtk4-rs](https://gtk-rs.org/gtk4-rs/git/docs)
10//! | [gtk-rs-core](https://gtk-rs.org/gtk-rs-core/git/docs)
11//! | [libadwaita-rs](https://world.pages.gitlab.gnome.org/Rust/libadwaita-rs/git/docs/libadwaita)
12//! | [libpanel-rs](https://world.pages.gitlab.gnome.org/Rust/libpanel-rs/git/docs/libpanel)
13//!
14//! [GitHub](https://github.com/Relm4/Relm4)
15//! | [Website](https://relm4.org)
16//! | [Book](https://relm4.org/book/stable/)
17//! | [Blog](https://relm4.org/blog)
18
19
20#![doc(html_logo_url = "https://relm4.org/icons/relm4_logo.svg")]
21#![doc(html_favicon_url = "https://relm4.org/icons/relm4_org.svg")]
22#![warn(
23 missing_debug_implementations,
24 missing_docs,
25 rust_2018_idioms,
26 unreachable_pub,
27 unused_qualifications,
28 clippy::cargo,
29 clippy::must_use_candidate
30)]
31
32use proc_macro::TokenStream;
33use syn::{ItemImpl, parse_macro_input};
34
35mod additional_fields;
36mod args;
37mod attrs;
38mod component;
39mod menu;
40mod view;
41mod visitors;
42mod widgets;
43
44#[macro_use]
45mod util;
46mod factory;
47mod token_streams;
48mod widget_template;
49
50use attrs::{Attrs, SyncOnlyAttrs};
51use menu::Menus;
52
53fn gtk_import() -> syn::Path {
54 if cfg!(feature = "relm4") {
55 util::strings_to_path(&["relm4", "gtk"])
56 } else {
57 util::strings_to_path(&["gtk"])
58 }
59}
60
61/// Macro that implements `relm4::Component` or `relm4::SimpleComponent`
62/// and generates the corresponding widgets struct.
63///
64/// # Attributes
65///
66/// To create public struct use `#[component(pub)]` or `#[component(visibility = pub)]`.
67///
68/// # Example
69///
70/// ```
71/// use relm4::prelude::*;
72/// use gtk::prelude::*;
73///
74/// #[derive(Default)]
75/// struct App {
76/// counter: u8,
77/// }
78///
79/// #[derive(Debug)]
80/// enum Msg {
81/// Increment,
82/// Decrement,
83/// }
84///
85/// #[relm4_macros::component(pub)]
86/// impl SimpleComponent for App {
87/// type Init = u8;
88/// type Input = Msg;
89/// type Output = ();
90///
91/// view! {
92/// gtk::Window {
93/// set_title: Some("Simple app"),
94/// set_default_size: (300, 100),
95/// gtk::Box {
96/// set_orientation: gtk::Orientation::Vertical,
97/// set_margin_all: 5,
98/// set_spacing: 5,
99///
100/// gtk::Button {
101/// set_label: "Increment",
102/// connect_clicked => Msg::Increment,
103/// },
104/// gtk::Button {
105/// set_label: "Decrement",
106/// connect_clicked[sender] => move |_| {
107/// sender.input(Msg::Decrement);
108/// },
109/// },
110/// gtk::Label {
111/// set_margin_all: 5,
112/// #[watch]
113/// set_label: &format!("Counter: {}", model.counter),
114/// }
115/// },
116/// }
117/// }
118///
119/// fn init(
120/// counter: Self::Init,
121/// root: Self::Root,
122/// sender: ComponentSender<Self>,
123/// ) -> ComponentParts<Self> {
124/// let model = Self { counter };
125///
126/// let widgets = view_output!();
127///
128/// ComponentParts { model, widgets }
129/// }
130///
131/// fn update(&mut self, msg: Msg, _sender: ComponentSender<Self>) {
132/// match msg {
133/// Msg::Increment => {
134/// self.counter = self.counter.wrapping_add(1);
135/// }
136/// Msg::Decrement => {
137/// self.counter = self.counter.wrapping_sub(1);
138/// }
139/// }
140/// }
141/// }
142/// ```
143///
144/// # Notes on `pre_view`
145///
146/// Using `return` in `pre_view` will cause a compiler warning.
147/// In general, you don't want to use `return` in `pre_view` as it will
148/// cause all following update functionality to be skipped.
149///
150/// ```compile_fail
151/// # use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt};
152/// # use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent, RelmWidgetExt};
153/// #
154/// struct App {}
155///
156/// #[relm4_macros::component]
157/// impl SimpleComponent for App {
158/// /* Code omitted */
159/// # type Init = ();
160/// # type Input = ();
161/// # type Output = ();
162/// #
163/// # view! {
164/// # gtk::Window {}
165/// # }
166///
167/// fn pre_view() {
168/// return;
169/// }
170/// #
171/// # fn init(
172/// # counter: Self::Init,
173/// # root: &Self::Root,
174/// # sender: ComponentSender<Self>,
175/// # ) -> ComponentParts<Self> {
176/// # let model = Self {};
177/// #
178/// # let widgets = view_output!();
179/// #
180/// # ComponentParts { model, widgets }
181/// # }
182/// }
183/// ```
184#[proc_macro_attribute]
185pub fn component(attributes: TokenStream, input: TokenStream) -> TokenStream {
186 let global_attributes: Attrs = parse_macro_input!(attributes);
187 let backup_input = input.clone();
188 let component_impl_res = syn::parse::<ItemImpl>(input);
189
190 match component_impl_res {
191 Ok(component_impl) => component::generate_tokens(global_attributes, component_impl).into(),
192 Err(_) => util::item_impl_error(backup_input),
193 }
194}
195
196/// Macro that implements `relm4::factory::FactoryComponent` and generates the corresponding widgets struct.
197///
198/// # Attributes
199///
200/// To create public struct use `#[factory(pub)]` or `#[factory(visibility = pub)]`.
201///
202/// # Example
203///
204/// ```
205/// use relm4::prelude::*;
206/// use relm4::factory::*;
207/// use gtk::prelude::*;
208///
209/// # #[derive(Debug)]
210/// # enum AppMsg {
211/// # AddCounter,
212/// # RemoveCounter,
213/// # SendFront(DynamicIndex)
214/// # }
215///
216/// #[derive(Debug)]
217/// struct Counter {
218/// value: u8,
219/// }
220///
221/// #[derive(Debug)]
222/// enum CounterMsg {
223/// Increment,
224/// Decrement,
225/// }
226///
227/// #[derive(Debug)]
228/// enum CounterOutput {
229/// SendFront(DynamicIndex),
230/// }
231///
232/// #[relm4_macros::factory(pub)]
233/// impl FactoryComponent for Counter {
234/// type CommandOutput = ();
235/// type Init = u8;
236/// type Input = CounterMsg;
237/// type Output = CounterOutput;
238/// type ParentWidget = gtk::Box;
239///
240///
241/// view! {
242/// root = gtk::Box {
243/// set_orientation: gtk::Orientation::Horizontal,
244/// set_spacing: 10,
245///
246/// #[name(label)]
247/// gtk::Label {
248/// #[watch]
249/// set_label: &self.value.to_string(),
250/// set_width_chars: 3,
251/// },
252///
253/// #[name(add_button)]
254/// gtk::Button {
255/// set_label: "+",
256/// connect_clicked => CounterMsg::Increment,
257/// },
258///
259/// #[name(remove_button)]
260/// gtk::Button {
261/// set_label: "-",
262/// connect_clicked => CounterMsg::Decrement,
263/// },
264///
265/// #[name(to_front_button)]
266/// gtk::Button {
267/// set_label: "To start",
268/// connect_clicked[sender, index] => move |_| {
269/// sender.output(CounterOutput::SendFront(index.clone()));
270/// }
271/// }
272/// }
273/// }
274///
275/// fn init_model(
276/// value: Self::Init,
277/// _index: &DynamicIndex,
278/// _sender: FactorySender<Self>,
279/// ) -> Self {
280/// Self { value }
281/// }
282///
283/// fn update(&mut self, msg: Self::Input, _sender: FactorySender<Self>) {
284/// match msg {
285/// CounterMsg::Increment => {
286/// self.value = self.value.wrapping_add(1);
287/// }
288/// CounterMsg::Decrement => {
289/// self.value = self.value.wrapping_sub(1);
290/// }
291/// }
292/// }
293/// }
294/// ```
295///
296/// Note: the enclosing App view (which has AppMsg as its input) is responsible for adding a
297/// forward handler to the `FactoryVecDeque`, which will translate CounterOutput events into AppMsg
298/// events. For example `CounterOutput::SendFront(index) => AppMsg::SendFront(index)`
299///
300#[proc_macro_attribute]
301pub fn factory(attributes: TokenStream, input: TokenStream) -> TokenStream {
302 let attrs = parse_macro_input!(attributes);
303 let backup_input = input.clone();
304 let factory_impl_res = syn::parse::<ItemImpl>(input);
305
306 match factory_impl_res {
307 Ok(factory_impl) => factory::generate_tokens(attrs, factory_impl).into(),
308 Err(_) => util::item_impl_error(backup_input),
309 }
310}
311
312/// A macro to create menus.
313///
314/// Use
315///
316/// + `"Label text" => ActionType,` to create new entries.
317/// + `"Label text" => ActionType(value),` to create new entries with action value.
318/// + `custom: "widget_id",` to add a placeholder for custom widgets you can add later with [`set_attribute_name`](https://gtk-rs.org/gtk-rs-core/stable/0.15/docs/gio/struct.MenuItem.html#method.set_attribute_value).
319/// + `section! { ... }` to create new sections.
320///
321/// # Example
322///
323/// ```
324/// # fn gettext(string: &str) -> String {
325/// # string.to_owned()
326/// # }
327/// #
328/// // Define some actions
329/// relm4::new_action_group!(WindowActionGroup, "win");
330/// relm4::new_stateless_action!(TestAction, WindowActionGroup, "test");
331/// relm4::new_stateful_action!(TestU8Action, WindowActionGroup, "test2", u8, u8);
332///
333/// // Create a `MenuModel` called `menu_model`
334/// relm4_macros::menu! {
335/// main_menu: {
336/// custom: "my_widget",
337/// // Translate with gettext-rs, for example.
338/// &gettext("Test") => TestAction,
339/// "Test2" => TestAction,
340/// "Test toggle" => TestU8Action(1_u8),
341/// section! {
342/// "Section test" => TestAction,
343/// "Test toggle" => TestU8Action(1_u8),
344/// },
345/// section! {
346/// "Test" => TestAction,
347/// "Test2" => TestAction,
348/// "Test Value" => TestU8Action(1_u8),
349/// }
350/// }
351/// };
352/// ```
353///
354/// # Macro expansion
355///
356/// The code generation for the example above looks like this (plus comments):
357///
358/// ```
359/// # fn gettext(string: &str) -> String {
360/// # string.to_owned()
361/// # }
362/// #
363/// struct WindowActionGroup;
364/// impl relm4::actions::ActionGroupName for WindowActionGroup {
365/// const NAME: &'static str = "win";
366/// }
367///
368/// struct TestAction;
369/// impl relm4::actions::ActionName for TestAction {
370/// type Group = WindowActionGroup;
371/// type State = ();
372/// type Target = ();
373///
374/// const NAME: &'static str = "test";
375/// }
376///
377/// struct TestU8Action;
378/// impl relm4::actions::ActionName for TestU8Action {
379/// type Group = WindowActionGroup;
380/// type State = u8;
381/// type Target = u8;
382///
383/// const NAME: &'static str = "test2";
384/// }
385///
386/// // Main menu
387/// let main_menu = relm4::gtk::gio::Menu::new();
388///
389/// // Placeholder for custom widget
390/// let new_entry = relm4::gtk::gio::MenuItem::new(None, None);
391/// let variant = relm4::gtk::glib::variant::ToVariant::to_variant("my_widget");
392/// new_entry.set_attribute_value("custom", Some(&variant));
393/// main_menu.append_item(&new_entry);
394///
395/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item(&gettext("Test"));
396/// main_menu.append_item(&new_entry);
397/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Test2");
398/// main_menu.append_item(&new_entry);
399/// let new_entry = relm4::actions::RelmAction::<TestU8Action>::to_menu_item_with_target_value(
400/// "Test toggle",
401/// &1_u8,
402/// );
403/// main_menu.append_item(&new_entry);
404///
405/// // Section 0
406/// let _section_0 = relm4::gtk::gio::Menu::new();
407/// main_menu.append_section(None, &_section_0);
408/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Section test");
409/// _section_0.append_item(&new_entry);
410/// let new_entry = relm4::actions::RelmAction::<TestU8Action>::to_menu_item_with_target_value(
411/// "Test toggle",
412/// &1_u8,
413/// );
414/// _section_0.append_item(&new_entry);
415///
416/// // Section 1
417/// let _section_1 = relm4::gtk::gio::Menu::new();
418/// main_menu.append_section(None, &_section_1);
419/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Test");
420/// _section_1.append_item(&new_entry);
421/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Test2");
422/// _section_1.append_item(&new_entry);
423/// let new_entry = relm4::actions::RelmAction::<TestU8Action>::to_menu_item_with_target_value(
424/// "Test Value",
425/// &1_u8,
426/// );
427/// _section_1.append_item(&new_entry);
428/// ```
429#[proc_macro]
430pub fn menu(input: TokenStream) -> TokenStream {
431 let menus = parse_macro_input!(input as Menus);
432 menus.menus_stream().into()
433}
434
435/// The [`view!`] macro allows you to construct your UI easily and cleanly.
436///
437/// It does the same as inside the [`macro@component`] attribute macro,
438/// but with less features.
439///
440/// You can even use the `relm4-macros` crate independently from Relm4 to build your GTK4 UI.
441///
442/// ```no_run
443/// use gtk::prelude::{BoxExt, ButtonExt};
444/// use relm4::gtk;
445///
446/// // Creating a box with a button inside.
447/// relm4_macros::view! {
448/// vbox = gtk::Box {
449/// gtk::Button {
450/// set_label: "Click me!",
451/// connect_clicked => |_| {
452/// println!("Hello world!");
453/// }
454/// },
455/// prepend: my_label = >k::Label::builder()
456/// .label("The view macro works!")
457/// .build(),
458/// }
459/// }
460///
461/// // You can simply use the vbox created in the macro.
462/// let spacing = vbox.spacing();
463/// ```
464///
465/// Also, the macro doesn't rely on any special gtk4-rs features
466/// so you can even use the macro for other purposes.
467///
468/// In this example, we use it to construct a [`Command`](std::process::Command).
469///
470/// ```
471/// use std::process::Command;
472///
473/// let path = "/";
474///
475/// relm4_macros::view! {
476/// mut process = Command::new("ls") {
477/// args: ["-la"],
478/// current_dir = mut &String {
479/// push_str: path,
480/// },
481/// env: ("HOME", "/home/relm4"),
482/// }
483/// }
484///
485/// // Output of "ls -la" at "/"
486/// dbg!(process.output());
487/// ```
488/// # Macro expansion
489///
490/// Let's have a look the this example:
491///
492/// ```no_run
493/// # use gtk::prelude::{BoxExt, ButtonExt};
494/// # use relm4::gtk;
495/// // Creating a box with a button inside.
496/// relm4_macros::view! {
497/// vbox = gtk::Box {
498/// gtk::Button {
499/// set_label: "Click me!",
500/// connect_clicked => |_| {
501/// println!("Hello world!");
502/// }
503/// },
504/// prepend: my_label = >k::Label::builder()
505/// .label("The view macro works!")
506/// .build(),
507/// }
508/// }
509/// ```
510///
511/// The code generation for this example looks like this (plus comments):
512///
513/// ```no_run
514/// # use gtk::prelude::{BoxExt, ButtonExt};
515/// # use relm4::gtk;
516///
517/// // We've just used `gtk::Box` so we assume it has a `default()` method
518/// let vbox = gtk::Box::default();
519/// // `vbox` was named, yet the button doesn't have an explicit name and gets a generated one instead.
520/// let _gtk_button_5 = gtk::Button::default();
521/// // For the label, we used a manual constructor method, so no `default()` method is required.
522/// let my_label = gtk::Label::builder().label("The view macro works!").build();
523///
524/// // Connect the signal
525/// {
526/// _gtk_button_5.connect_clicked(|_| {
527/// println!("Hello world!");
528/// });
529/// }
530///
531/// // The button was added without any further instructions, so we assume `container_add()` will work.
532/// relm4::RelmContainerExt::container_add(&vbox, &_gtk_button_5);
533/// _gtk_button_5.set_label("Click me!");
534/// // For the label, we used the `prepend` method, so we don't need `container_add()` here.
535/// vbox.prepend(&my_label);
536/// ```
537///
538/// The widgets are first initialized, then signals are connected and then
539/// properties and widgets are assigned to each other.
540///
541/// The nested structure of the UI is translated into regular Rust code.
542#[proc_macro]
543pub fn view(input: TokenStream) -> TokenStream {
544 view::generate_tokens(input)
545}
546
547/// A macro to generate widget templates.
548///
549/// This macro generates a new type that implements `relm4::WidgetTemplate`.
550///
551/// # Example
552///
553/// ```
554/// use relm4::prelude::*;
555/// use gtk::prelude::*;
556///
557/// #[relm4::widget_template]
558/// impl WidgetTemplate for MyBox {
559/// view! {
560/// gtk::Box {
561/// set_margin_all: 10,
562/// // Make the boxes visible
563/// inline_css: "border: 2px solid blue",
564/// }
565/// }
566/// }
567/// ```
568///
569/// The template allows you the generate deeply nested
570/// structures. All named items will be directly accessible
571/// as a child of the template, even if they are nested.
572/// In this example the "child_label" is a template child.
573///
574/// ```
575/// # use relm4::prelude::*;
576/// # use gtk::prelude::*;
577/// #
578/// # #[relm4::widget_template]
579/// # impl WidgetTemplate for MyBox {
580/// # view! {
581/// # gtk::Box {
582/// # set_margin_all: 10,
583/// # // Make the boxes visible
584/// # inline_css: "border: 2px solid blue",
585/// # }
586/// # }
587/// # }
588/// #
589/// #[relm4::widget_template]
590/// impl WidgetTemplate for MySpinner {
591/// view! {
592/// gtk::Spinner {
593/// set_spinning: true,
594/// }
595/// }
596/// }
597///
598/// #[relm4::widget_template]
599/// impl WidgetTemplate for CustomBox {
600/// view! {
601/// gtk::Box {
602/// set_orientation: gtk::Orientation::Vertical,
603/// set_margin_all: 5,
604/// set_spacing: 5,
605///
606/// #[template]
607/// MyBox {
608/// #[template]
609/// MySpinner,
610///
611/// #[template]
612/// MyBox {
613/// #[template]
614/// MySpinner,
615///
616/// #[template]
617/// MyBox {
618/// #[template]
619/// MySpinner,
620///
621/// // Deeply nested!
622/// #[name = "child_label"]
623/// gtk::Label {
624/// set_label: "This is a test",
625/// }
626/// }
627/// }
628/// }
629/// }
630/// }
631/// }
632/// ```
633#[proc_macro_attribute]
634pub fn widget_template(attributes: TokenStream, input: TokenStream) -> TokenStream {
635 let SyncOnlyAttrs { visibility } = parse_macro_input!(attributes);
636
637 let item_impl = parse_macro_input!(input as ItemImpl);
638 widget_template::generate_tokens(visibility, item_impl).into()
639}
640
641#[cfg(test)]
642#[rustversion::all(stable, since(1.72))]
643mod test {
644 #[test]
645 fn ui() {
646 let t = trybuild::TestCases::new();
647 t.compile_fail("tests/ui/compile-fail/**/*.rs");
648 }
649}