From 6446d43d76fb41fd2a3107c5f1b764b6f581f328 Mon Sep 17 00:00:00 2001 From: Reinout Meliesie Date: Fri, 2 Jan 2026 13:41:27 +0100 Subject: [PATCH 1/2] Replace custom widget macros with Relm4 view macro based ones Also move margin styling to the CSS file. --- Cargo.lock | 12 + Cargo.toml | 1 + src/application.css | 11 +- src/ui/collatable_container/collated_grid.rs | 169 +++++----- .../collation_menu/mod.rs | 17 +- .../collation_menu/sort_button.rs | 62 ++-- src/ui/collatable_container/mod.rs | 22 +- src/ui/mod.rs | 143 +++++---- src/ui/utility.rs | 297 ++---------------- 9 files changed, 299 insertions(+), 435 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa823d5..1f2b7f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "relm4-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25edbb5b2e8126975f1dd8e85c48cd310afc150beed0dc97df22247b3243971e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -818,4 +829,5 @@ dependencies = [ "futures", "gtk4", "libadwaita", + "relm4-macros", ] diff --git a/Cargo.toml b/Cargo.toml index 4c9251d..f12ced3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ fallible-iterator = "0.3.0" # Must match version used by async-sqlite futures = "0.3.31" gtk4 = { version = "0.10.3" , features = [ "v4_20" ] } libadwaita = { version = "0.8.1" , features = [ "v1_8" ] } +relm4-macros = { version = "0.10.1" , default-features = false } diff --git a/src/application.css b/src/application.css index 48e26c2..3909e08 100644 --- a/src/application.css +++ b/src/application.css @@ -11,10 +11,19 @@ padding : 0 ; } -.open-collection-item-button { +.collection-item-button { font-weight : normal ; /* No bold text by default for this kind of button */ } +.collection-item-box { + margin-top : 20px ; + margin-bottom : 20px ; +} + +.collection-item-image { + margin-bottom : 20px ; +} + .media-modal { padding : 100px ; } diff --git a/src/ui/collatable_container/collated_grid.rs b/src/ui/collatable_container/collated_grid.rs index c30e2d9..d34dd31 100644 --- a/src/ui/collatable_container/collated_grid.rs +++ b/src/ui/collatable_container/collated_grid.rs @@ -1,4 +1,4 @@ -use gtk4 :: * ; +use gtk4 :: { Button , FlowBox , Image , Justification , Label , SelectionMode } ; use gtk4 :: Align :: * ; use gtk4 :: Orientation :: * ; use gtk4 :: gdk :: * ; @@ -24,12 +24,14 @@ pub struct CollatedMediaGrid < A : MediaAdapter > { impl < A : MediaAdapter > CollatedMediaGrid { pub fn new ( on_media_selected : impl Fn ( A :: Overview ) + 'static ) -> Self { - let grid_widget = flow_box ! ( - @ orientation : Horizontal ; - @ homogeneous : true ; - @ css_classes : & [ "collatable-container" ] ; - @ selection_mode : SelectionMode :: None ; - ) ; + let grid_widget = view_expr ! { + FlowBox { + set_homogeneous : true , + set_selection_mode : SelectionMode :: None , + set_css_classes : & [ "collatable-container" ] , + set_orientation : Horizontal , + } + } ; let media_widget_pairs = RefCell :: new ( Vec :: new () ) ; let on_media_selected = leak (on_media_selected) ; @@ -51,82 +53,99 @@ impl < A : MediaAdapter > CollatedMediaGrid { } async fn create_media_entry ( & self , media : & A :: Overview ) -> Button { - button ! ( - @ css_classes : & [ "flat" , "open-collection-item-button" ] ; - @ connect_clicked : clone ! ( - # [strong] media , - # [ strong ( rename_to = on_media_selected ) ] self . on_media_selected , - move |_| on_media_selected ( media . clone () ) , - ) ; - & g_box ! ( - @ option_children ; - @ orientation : Vertical ; - @ valign : Center ; // I.e. do not fill parent vertically - @ margin_top : 20 ; - @ margin_bottom : 20 ; + view_expr ! { + Button { + set_css_classes : & [ "flat" , "collection-item-button" ] , - { - let home_directory = var_os ("HOME") . unwrap () ; - let xdg_data_home = var_os ("XDG_DATA_HOME") ; + connect_clicked : clone ! ( + # [ strong ] media , + # [ strong ( rename_to = on_media_selected ) ] self . on_media_selected , + move |_| on_media_selected ( media . clone () ) , + ) , - let data_dir = match xdg_data_home { - Some (xdg_data_home) => concat_os_str ! ( xdg_data_home , "/zoodex" ) , - None => concat_os_str ! ( home_directory , "/.local/share/zoodex" ) , - } ; + set_child : Some ( & view_expr ! { + gtk4 :: Box { + set_css_classes : & [ "collection-item-box" ] , + set_valign : Center , + set_orientation : Vertical , - let poster_file_path = concat_os_str ! ( data_dir , "/posters/" , media . get_uuid () ) ; + // Poster + append_opt : & { + let home_directory = var_os ("HOME") . unwrap () ; + let xdg_data_home = var_os ("XDG_DATA_HOME") ; - let poster_texture = spawn_blocking ( - move || Texture :: from_filename (poster_file_path) , - ) . await . unwrap () ; + let data_dir = match xdg_data_home { + Some (xdg_data_home) => concat_os_str ! ( xdg_data_home , "/zoodex" ) , + None => concat_os_str ! ( home_directory , "/.local/share/zoodex" ) , + } ; - match poster_texture { - Ok (poster_texture) => Some ( image ! ( - @ margin_bottom : 10 ; - @ pixel_size : 300 ; - @ paintable : & poster_texture ; - ) ) , - Err (error) => { - if error . matches ( IOErrorEnum :: NotFound ) { - None // The file not existing simply means there is no poster for this piece of media - } else { - panic ! ( "{}" , error ) // Any other error means something unexpected went wrong + let poster_file_path = concat_os_str ! ( data_dir , "/posters/" , media . get_uuid () ) ; + + let poster_texture = spawn_blocking ( + move || Texture :: from_filename (poster_file_path) , + ) . await . unwrap () ; + + match poster_texture { + Ok (poster_texture) => Some ( view_expr ! { + Image { + set_paintable : Some ( & poster_texture ) , + set_pixel_size : 300 , + set_css_classes : & [ "collection-item-image" ] , + } + } ) , + Err (error) => { + if error . matches ( IOErrorEnum :: NotFound ) { + None // The file not existing simply means there is no poster for this piece of media + } else { + panic ! ( "{}" , error ) // Any other error means something unexpected went wrong + } + } , + } + } , + + // Name + append : & view_expr ! { + Label { + set_attributes : Some ( & pango_attributes ! ( scale : SCALE_LARGE , weight : Bold ) ) , + set_justify : Justification :: Center , + set_max_width_chars : 1 , // Not the actual limit, used instead to wrap more aggressively + set_wrap : true , + set_label : media . get_name () . as_str () , + } + } , + + // Original name + append_opt : & media . get_original_name () . map ( |original_name| view_expr ! { + Label { + set_justify : Justification :: Center , + set_max_width_chars : 1 , + set_wrap : true , + set_label : original_name . as_str () , + } + } ) , + + // Details + append : & view_expr ! { + gtk4 :: Box { + set_spacing : 20 , + set_halign : Center , + set_orientation : Horizontal , + + // Release date + append : & view_expr ! { + Label { set_label : media . get_release_date () . split ('-') . next () . unwrap () } + } , + + // Runtime + append_opt : & media . get_runtime_minutes () . map ( |runtime_minutes| view_expr ! { + Label { set_label : format ! ( "{}m" , runtime_minutes ) . as_str () } + } ) , } } , } - } . as_ref () , - - Some ( & label ! ( - @ justify : Justification :: Center ; - @ wrap : true ; - @ max_width_chars : 1 ; // Not the actual limit, used instead to wrap more aggressively - @ attributes : & pango_attributes ! ( @ scale : SCALE_LARGE ; @ weight : Bold ; ) ; - media . get_name () . as_str () , - ) ) , - - media . get_original_name () . map ( |original_name| label ! ( - @ justify : Justification :: Center ; - @ wrap : true ; - @ max_width_chars : 1 ; // Not the actual limit, used instead to wrap more aggressively - original_name . as_str () , - ) ) . as_ref () , - - Some ( & g_box ! ( - @ option_children ; - @ orientation : Horizontal ; - @ halign : Center ; - @ spacing : 20 ; - - Some ( & label ! ( - media . get_release_date () . split ('-') . next () . unwrap () , - ) ) , - - media . get_runtime_minutes () . map ( - |runtime_minutes| label ! ( format ! ( "{}m" , runtime_minutes ) . as_str () ) , - ) . as_ref () , - ) ) , - ) , - ) + } ) , + } + } } pub fn set_sorting ( & self , sorting : A :: Sorting ) { diff --git a/src/ui/collatable_container/collation_menu/mod.rs b/src/ui/collatable_container/collation_menu/mod.rs index ccc0e3f..492b080 100644 --- a/src/ui/collatable_container/collation_menu/mod.rs +++ b/src/ui/collatable_container/collation_menu/mod.rs @@ -19,13 +19,16 @@ impl MediaCollationMenu { pub fn new < A : MediaAdapter > ( on_sort : impl Fn ( A :: Sorting ) + 'static ) -> Self { let sort_button = MediaSortButton :: :: new (on_sort) ; - let widget = g_box ! ( - @ orientation : Horizontal ; - @ halign : Center ; - @ spacing : 20 ; - @ css_classes : & [ "toolbar" , "collation-menu" ] ; - sort_button . get_widget () , - ) ; + let widget = view_expr ! { + gtk4 :: Box { + set_spacing : 20 , + set_css_classes : & [ "toolbar" , "collation-menu" ] , + set_halign : Center , + set_orientation : Horizontal , + + append : sort_button . get_widget () , + } + } ; Self { widget } } diff --git a/src/ui/collatable_container/collation_menu/sort_button.rs b/src/ui/collatable_container/collation_menu/sort_button.rs index e748e1f..920a213 100644 --- a/src/ui/collatable_container/collation_menu/sort_button.rs +++ b/src/ui/collatable_container/collation_menu/sort_button.rs @@ -1,6 +1,6 @@ -use gtk4 :: Image ; +use gtk4 :: { Image , ListBox , Popover } ; use gtk4 :: Align :: * ; -use libadwaita :: * ; +use libadwaita :: SplitButton ; use std :: cell :: * ; use crate :: utility :: * ; @@ -23,31 +23,53 @@ impl < A : MediaAdapter > MediaSortButton { let sort_icons = { let mut sort_icons = Vec :: new () ; for _ in property_descriptions { - sort_icons . push ( image ! ( @ icon_name : "view-sort-ascending-symbolic" ; ) ) ; + sort_icons . push ( view_expr ! { + Image { set_icon_name : Some ( "view-sort-ascending-symbolic" ) } + } ) ; } Box :: leak ( sort_icons . into_boxed_slice () ) as & 'static _ } ; - let list_box = list_box ! ( - @ connect_row_activated : move | _ , row | on_media_sort_activated :: ( - row . index () , - previous_sorting , - & on_sort , - sort_icons , - ) ; - ) ; + let list_box = view_expr ! { + ListBox { + connect_row_activated : move | _ , row | on_media_sort_activated :: ( + row . index () , + previous_sorting , + & on_sort , + sort_icons , + ) , + } + } ; for ( index , ( _ , description ) ) in property_descriptions . iter () . enumerate () { - list_box . append ( & g_box ! ( - @ orientation : Horizontal ; @ spacing : 20 ; - & label ! ( @ hexpand : true ; @ halign : Start ; description ) , - & sort_icons [index] , - ) ) ; + list_box . append ( & view_expr ! { + gtk4 :: Box { + set_spacing : 20 , + set_orientation : Horizontal , + append : & view_expr ! { + Label { + set_halign : Start , + set_hexpand : true , + set_label : description , + } + } , + append : & sort_icons [index] , + } + } ) ; } - let widget = split_button ! ( - @ popover : & popover ! ( @ css_classes : & [ "menu" ] ; & list_box ) ; - & label ! ("Sort") , - ) ; + let widget = view_expr ! { + SplitButton { + set_popover : Some ( & view_expr ! { + Popover { + set_css_classes : & [ "menu" ] , + set_child : Some ( & list_box ) , + } + } ) , + set_child : Some ( & view_expr ! { + Label { set_label : "Sort" } + } ) , + } + } ; Self { widget , previous_sorting } } diff --git a/src/ui/collatable_container/mod.rs b/src/ui/collatable_container/mod.rs index 7e3ae07..cc1ceaf 100644 --- a/src/ui/collatable_container/mod.rs +++ b/src/ui/collatable_container/mod.rs @@ -1,7 +1,7 @@ mod collated_grid ; mod collation_menu ; -use gtk4 :: Box ; +use gtk4 :: { Box , ScrolledWindow } ; use gtk4 :: Orientation :: * ; use gtk4 :: prelude :: * ; use std :: cmp :: * ; @@ -73,14 +73,18 @@ impl < A : MediaAdapter > CollatableMediaContainer { |sorting| collated_grid . set_sorting (sorting) , ) ; - let widget = g_box ! ( - @ orientation : Vertical ; - collation_menu . get_widget () , - & scrolled_window ! ( - @ propagate_natural_height : true ; - & vertically_filling ! ( collated_grid . get_widget () ) , - ) , - ) ; + let widget = view_expr ! { + gtk4 :: Box { + set_orientation : Vertical , + append : collation_menu . get_widget () , + append : & view_expr ! { + ScrolledWindow { + set_propagate_natural_height : true , + set_child : Some ( & vertical_filler ( collated_grid . get_widget () ) ) , + } + } , + } + } ; Self { collated_grid, widget } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1495342..1444ecc 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,11 +3,14 @@ mod component ; mod utility ; use futures :: * ; +use gtk4 :: { Button , Image , Label } ; use gtk4 :: Orientation :: * ; +use gtk4 :: glib :: * ; use gtk4 :: prelude :: * ; -use libadwaita :: * ; -use libadwaita :: prelude :: * ; +use libadwaita :: { Application , ApplicationWindow , Dialog , HeaderBar , ToolbarView , ViewStack , ViewSwitcher } ; use libadwaita :: ViewSwitcherPolicy :: * ; +use libadwaita :: prelude :: * ; +use relm4_macros :: * ; use std :: process :: * ; use crate :: data_manager :: * ; @@ -31,70 +34,91 @@ impl UI { let get_film_details = leak (get_film_details) ; let films_component = CollatableMediaContainer :: :: new ( |film| { - glib :: spawn_future_local ( async { + spawn_future_local ( async { let film_details = get_film_details ( film . uuid ) . await ; - dialog ! ( - & g_box ! ( - @ option_children ; - @ orientation : Vertical ; - @ spacing : 40 ; - @ css_classes : & [ "media-modal" ] ; + view ! { + Dialog { + present : Some ( & window . libadwaita_window ) , + set_child : Some ( & view_expr ! { + gtk4 :: Box { + set_spacing : 40 , + set_css_classes : & [ "media-modal" ] , + set_orientation : Vertical , - Some ( label ! ( - @ css_classes : & [ "title-1" ] ; - film_details . name . as_str () , - ) ) . as_ref () , + append : & view_expr ! { + Label { + set_css_classes : & [ "title-1" ] , + set_label : film_details . name . as_str () , + } + } , - film_details . original_name . map ( - |original_name| label ! ( original_name . as_str () ) , - ) . as_ref () , + append_opt : & film_details . original_name . map ( |original_name| view_expr ! { + Label { set_label : original_name . as_str () } + } ) , - Some ( label ! ( - & format ! ( "Release date: {}" , film_details . release_date ) , - ) ) . as_ref () , + append : & view_expr ! { + Label { set_label : & format ! ( "Release date: {}" , film_details . release_date ) } + } , - film_details . source . map ( - |source| button ! ( - @ css_classes : & [ "suggested-action" , "circular" ] ; - @ connect_clicked : move |_| { - let source = source . clone () ; + append_opt : & film_details . source . map ( |source| view_expr ! { + Button { + set_css_classes : & [ "suggested-action" , "circular" ] , - let arguments = [ - Some ( source . file_path . as_os_str () . to_owned () ) , - source . audio_track . map ( - |audio_track| concat_os_str ! ( "--mpv-aid=" , to_os_string (audio_track) ) , - ) , - source . subtitle_track . map ( - |subtitle_track| concat_os_str ! ( "--mpv-sid=" , to_os_string (subtitle_track) ) , - ) , - ] . iter () . filter_map ( Option :: clone ) . collect :: < Vec <_> > () ; + connect_clicked : move |_| { + let arguments = [ + Some ( source . file_path . as_os_str () . to_owned () ) , + source . audio_track . map ( + |audio_track| concat_os_str ! ( "--mpv-aid=" , to_os_string (audio_track) ) , + ) , + source . subtitle_track . map ( + |subtitle_track| concat_os_str ! ( "--mpv-sid=" , to_os_string (subtitle_track) ) , + ) , + ] . iter () . filter_map ( Option :: clone ) . collect :: < Vec <_> > () ; - Command :: new ("/usr/bin/celluloid") . args (arguments) . spawn () - . unwrap () ; // TODO: Better error handling for UI callbacks in general - } ; - & image ! ( @ icon_name : "media-playback-start-symbolic" ; ) , - ) , - ) . as_ref () , - ) , - ) . present ( Some ( & window . libadwaita_window ) ) + // TODO: Better error handling for UI callbacks in general + Command :: new ("/usr/bin/celluloid") . args (arguments) . spawn () . unwrap () ; + } , + set_child : Some ( & view_expr ! { + Image { set_icon_name : Some ("media-playback-start-symbolic") } + } ) , + } + } ) , + } + } ) , + } + } } ) ; } ) ; - let series_component = CollatableMediaContainer :: :: new ( - |series| dialog ! () . present ( Some ( & window . libadwaita_window ) ) , - ) ; - let switched = view_stack ! ( - ( "Films" , "camera-video-symbolic" , films_component . get_widget () ) , - ( "Series" , "video-display-symbolic" , series_component . get_widget () ) , - ) ; - let header_bar = header_bar ! ( - & view_switcher ! ( @ policy : Wide ; & switched ) , - ) ; + let series_component = CollatableMediaContainer :: :: new ( |series| { + view_expr ! { + Dialog { present : Some ( & window . libadwaita_window ) } + } ; + } ) ; + let switched = view_expr ! { + ViewStack { + add_titled_with_icon : ( films_component . get_widget () , None , "Films" , "camera-video-symbolic" ) , + add_titled_with_icon : ( series_component . get_widget () , None , "Series" , "video-display-symbolic" ) , + } + } ; + let header_bar = view_expr ! { + HeaderBar { + set_title_widget : Some ( & view_expr ! { + ViewSwitcher { + set_policy : Wide , + set_stack : Some ( & switched ) , + } + } ) , + } + } ; - window . libadwaita_window . set_content ( Some ( - & toolbar_view ! ( @ top_bar : & header_bar ; & switched ) , - ) ) ; + window . libadwaita_window . set_content ( Some ( & view_expr ! { + ToolbarView { + add_top_bar : & header_bar , + set_content : Some ( & switched ) , + } + } ) ) ; UI { films_component , series_component } } @@ -115,10 +139,13 @@ pub struct Window { impl Window { pub fn new (application : & Application ) -> Self { - let libadwaita_window = ApplicationWindow :: builder () - . application (application) - . title ("Zoödex") - . build () ; + let libadwaita_window = view_expr ! { + ApplicationWindow { + set_application : Some ( application ) , + set_title : Some ( "Zoödex" ) , + } + } ; + Self { libadwaita_window } } diff --git a/src/ui/utility.rs b/src/ui/utility.rs index 8dbffaf..ec310b2 100644 --- a/src/ui/utility.rs +++ b/src/ui/utility.rs @@ -1,269 +1,52 @@ -// Widget macros +use gtk4 :: Widget ; +use gtk4 :: Orientation :: * ; +use gtk4 :: prelude :: * ; +use libadwaita :: Bin ; -macro_rules ! bin { ( $ ( @ vexpand : $ vexpand : expr ; ) ? ) => { { - let widget = libadwaita :: Bin :: new () ; - $ ( widget . set_vexpand ( $ vexpand ) ; ) ? - widget -} } } -macro_rules ! button { ( - $ ( @ halign : $ halign : expr ; ) ? - $ ( @ valign : $ valign : expr ; ) ? - $ ( @ hexpand : $ hexpand : expr ; ) ? - $ ( @ vexpand : $ vexpand : expr ; ) ? - $ ( @ css_classes : $ css_classes : expr ; ) ? - $ ( @ connect_clicked : $ connect_clicked : expr ; ) ? - $ ( $ child : expr ) ? $ (,) ? -) => { { - let button = gtk4 :: Button :: new () ; - $ ( button . set_halign ( $ halign ) ; ) ? - $ ( button . set_valign ( $ valign ) ; ) ? - $ ( button . set_hexpand ( $ hexpand ) ; ) ? - $ ( button . set_vexpand ( $ vexpand ) ; ) ? - $ ( button . set_css_classes ( $ css_classes ) ; ) ? - $ ( button . connect_clicked ( $ connect_clicked ) ; ) ? - $ ( button . set_child ( Some ( $ child ) ) ; ) ? - button -} } } -macro_rules ! dialog { ( - $ ( @ css_classes : $ css_classes : expr ; ) ? - $ ( $ child : expr $ (,) ? ) ? -) => { { - let widget = libadwaita :: Dialog :: new () ; - $ ( widget . set_css_classes ( $ css_classes ) ; ) ? - $ ( widget . set_child ( Some ( $ child ) ) ; ) ? - widget -} } } +// Convenience function to conditionally append child to a widget -macro_rules ! flow_box { ( - $ ( @ orientation : $ orientation : expr ; ) ? - $ ( @ homogeneous : $ homogeneous : expr ; ) ? - $ ( @ css_classes : $ css_classes : expr ; ) ? - $ ( @ selection_mode : $ selection_mode : expr ; ) ? -) => { { - let widget = gtk4 :: FlowBox :: new () ; - $ ( widget . set_orientation ( $ orientation ) ; ) ? - $ ( widget . set_homogeneous ( $ homogeneous ) ; ) ? - $ ( widget . set_css_classes ( $ css_classes ) ; ) ? - $ ( widget . set_selection_mode ( $ selection_mode ) ; ) ? - widget -} } } - -macro_rules ! g_box { - ( - $ ( @ orientation : $ orientation : expr ; ) ? - $ ( @ halign : $ halign : expr ; ) ? - $ ( @ valign : $ valign : expr ; ) ? - $ ( @ spacing : $ spacing : expr ; ) ? - $ ( @ margin_top : $ margin_top : expr ; ) ? - $ ( @ margin_bottom : $ margin_bottom : expr ; ) ? - $ ( @ hexpand : $ hexpand : expr ; ) ? - $ ( @ vexpand : $ vexpand : expr ; ) ? - $ ( @ widget_name : $ widget_name : expr ; ) ? - $ ( @ css_classes : $ css_classes : expr ; ) ? - $ ( $ child : expr ) , * $ (,) ? - ) => { { - let container = gtk4 :: Box :: builder () . build () ; - $ ( container . set_orientation ( $ orientation ) ; ) ? - $ ( container . set_halign ( $ halign ) ; ) ? - $ ( container . set_valign ( $ valign ) ; ) ? - $ ( container . set_spacing ( $ spacing ) ; ) ? - $ ( container . set_margin_top ( $ margin_top ) ; ) ? - $ ( container . set_margin_bottom ( $ margin_bottom ) ; ) ? - $ ( container . set_hexpand ( $ hexpand ) ; ) ? - $ ( container . set_vexpand ( $ vexpand ) ; ) ? - $ ( container . set_widget_name ( $ widget_name ) ; ) ? - $ ( container . set_css_classes ( $ css_classes ) ; ) ? - $ ( container . append ( $ child ) ; ) * - container - } } ; - ( - @ option_children ; - $ ( @ orientation : $ orientation : expr ; ) ? - $ ( @ halign : $ halign : expr ; ) ? - $ ( @ valign : $ valign : expr ; ) ? - $ ( @ spacing : $ spacing : expr ; ) ? - $ ( @ margin_top : $ margin_top : expr ; ) ? - $ ( @ margin_bottom : $ margin_bottom : expr ; ) ? - $ ( @ hexpand : $ hexpand : expr ; ) ? - $ ( @ vexpand : $ vexpand : expr ; ) ? - $ ( @ widget_name : $ widget_name : expr ; ) ? - $ ( @ css_classes : $ css_classes : expr ; ) ? - $ ( $ child : expr ) , * $ (,) ? - ) => { { - let container = gtk4 :: Box :: builder () . build () ; - $ ( container . set_orientation ( $ orientation ) ; ) ? - $ ( container . set_halign ( $ halign ) ; ) ? - $ ( container . set_valign ( $ valign ) ; ) ? - $ ( container . set_spacing ( $ spacing ) ; ) ? - $ ( container . set_margin_top ( $ margin_top ) ; ) ? - $ ( container . set_margin_bottom ( $ margin_bottom ) ; ) ? - $ ( container . set_hexpand ( $ hexpand ) ; ) ? - $ ( container . set_vexpand ( $ vexpand ) ; ) ? - $ ( container . set_widget_name ( $ widget_name ) ; ) ? - $ ( container . set_css_classes ( $ css_classes ) ; ) ? - $ ( if let Some (child) = $ child { container . append (child) ; } ) * - container - } } ; +pub trait OptChildExt { + fn append_opt ( & self , child : & Option < impl IsA > ) ; } -macro_rules ! header_bar { ( $ title_widget : expr $ (,) ? ) => { { - let widget = libadwaita :: HeaderBar :: new () ; - widget . set_title_widget ( Some ( $ title_widget ) ) ; - widget -} } } +impl OptChildExt for gtk4 :: Box { + fn append_opt ( & self , child : & Option < impl IsA > ) { + if let Some (child) = child { + self . append (child) ; + } + } +} -macro_rules ! image { ( - $ ( @ halign : $ halign : expr ; ) ? - $ ( @ valign : $ valign : expr ; ) ? - $ ( @ width_request : $ width_request : expr ; ) ? - $ ( @ height_request : $ height_request : expr ; ) ? - $ ( @ margin_top : $ margin_top : expr ; ) ? - $ ( @ margin_bottom : $ margin_bottom : expr ; ) ? - $ ( @ pixel_size : $ pixel_size : expr ; ) ? - $ ( @ icon_name : $ icon_name : expr ; ) ? - $ ( @ paintable : $ paintable : expr ; ) ? -) => { { - let image = gtk4 :: Image :: new () ; - $ ( image . set_halign ( $ halign ) ; ) ? - $ ( image . set_valign ( $ valign ) ; ) ? - $ ( image . set_width_request ( $ width_request ) ; ) ? - $ ( image . set_height_request ( $ height_request ) ; ) ? - $ ( image . set_margin_top ( $ margin_top ) ; ) ? - $ ( image . set_margin_bottom ( $ margin_bottom ) ; ) ? - $ ( image . set_pixel_size ( $ pixel_size ) ; ) ? - $ ( image . set_icon_name ( Some ( $ icon_name ) ) ; ) ? - $ ( image . set_paintable ( Some ( $ paintable ) ) ; ) ? - image -} } } -macro_rules ! label { ( - $ ( @ hexpand : $ hexpand : expr ; ) ? - $ ( @ vexpand : $ vexpand : expr ; ) ? - $ ( @ halign : $ halign : expr ; ) ? - $ ( @ valign : $ valign : expr ; ) ? - $ ( @ justify : $ justify : expr ; ) ? - $ ( @ wrap : $ wrap : expr ; ) ? - $ ( @ max_width_chars : $ max_width_chars : expr ; ) ? - $ ( @ attributes : $ attributes : expr ; ) ? - $ ( @ css_classes : $ css_classes : expr ; ) ? - $ ( $ label : expr ) ? $ (,) ? -) => { { - let label = gtk4 :: Label :: builder () . build () ; - $ ( label . set_hexpand ( $ hexpand ) ; ) ? - $ ( label . set_vexpand ( $ vexpand ) ; ) ? - $ ( label . set_halign ( $ halign ) ; ) ? - $ ( label . set_valign ( $ valign ) ; ) ? - $ ( label . set_label ( $ label ) ; ) ? - $ ( label . set_justify ( $ justify ) ; ) ? - $ ( label . set_max_width_chars ( $ max_width_chars ) ; ) ? - $ ( label . set_attributes ( Some ( $ attributes ) ) ; ) ? - $ ( label . set_css_classes ( $ css_classes ) ; ) ? - $ ( label . set_wrap ( $ wrap ) ; ) ? - label -} } } -macro_rules ! list_box { ( - $ ( @ connect_row_activated : $ connect_row_activated : expr ; ) ? - $ ( $ child : expr ) , * $ (,) ? -) => { { - let container = gtk4 :: ListBox :: new () ; - $ ( container . connect_row_activated ( $ connect_row_activated ) ; ) ? - $ ( container . append ( $ child ) ; ) * - container -} } } +// The `view` macro from Relm4 as an expression instead of a variable declaration -macro_rules ! popover { ( - $ ( @ css_classes : $ css_classes : expr ; ) ? - $ child : expr $ (,) ? -) => { { - let widget = gtk4 :: Popover :: new () ; - $ ( widget . set_css_classes ( $ css_classes ) ; ) ? - widget . set_child ( Some ( $ child ) ) ; - widget -} } } - -macro_rules ! scrolled_window { ( - $ ( @ propagate_natural_height : $ propagate_natural_height : expr ; ) ? - $ child : expr $ (,) ? -) => { { - let widget = gtk4 :: ScrolledWindow :: new () ; - $ ( widget . set_propagate_natural_height ( $ propagate_natural_height ) ; ) ? - widget . set_child ( Some ( $ child ) ) ; - widget -} } } - -macro_rules ! split_button { ( - $ ( @ popover : $ popover : expr ; ) ? - $ child : expr $ (,) ? -) => { { - let widget = libadwaita :: SplitButton :: new () ; - $ ( widget . set_popover ( Some ( $ popover ) ) ; ) ? - widget . set_child ( Some ( $ child ) ) ; - widget -} } } - -macro_rules ! toolbar_view { ( - $ ( @ top_bar : $ top_bar : expr ; ) ? - $ ( $ content : expr ) ? $ (,) ? -) => { { - let widget = libadwaita :: ToolbarView :: new () ; - $ ( widget . add_top_bar ( $ top_bar ) ; ) ? - $ ( widget . set_content ( Some ( $ content ) ) ; ) ? - widget -} } } - -macro_rules ! view_stack { ( - $ ( ( $ title : expr , $ icon : expr , $ widget : expr $ (,) ? ) ) , * $ (,) ? -) => { { - let container = libadwaita :: ViewStack :: new () ; - $ ( container . add_titled_with_icon ( $ widget , None , $ title , $ icon ) ; ) * - container -} } } - -macro_rules ! view_switcher { ( - $ ( @ policy : $ policy : expr ; ) ? - $ stack : expr $ (,) ? -) => { { - let widget = libadwaita :: ViewSwitcher :: new () ; - $ ( widget . set_policy ( $ policy ) ; ) ? - widget . set_stack ( Some ( $ stack ) ) ; - widget +macro_rules ! view_expr { ( $ ( $ contents : tt ) * ) => { { + relm4_macros :: view ! { outer = $ ( $ contents ) * } + outer } } } -// Convenience widget macros - -macro_rules ! vertically_filling { ( $ child : expr ) => { - g_box ! ( - @ orientation : gtk4 :: Orientation :: Vertical ; - $ child , - & bin ! ( @ vexpand : true ; ) , - ) -} } +pub fn vertical_filler ( child : & impl IsA ) -> gtk4 :: Box { + view_expr ! { + gtk4 :: Box { + set_orientation : Vertical , + append : child , + append : & view_expr ! { + Bin { set_vexpand : true } + } , + } + } +} -// Other UI macros - -macro_rules ! application_window { ( - @ application : $ application : expr ; - @ title : $ title : expr ; - $ ( $ content : expr $ (,) ? ) ? -) => { { - use libadwaita :: prelude :: * ; - - let window = libadwaita :: ApplicationWindow :: new ( $ application ) ; - window . set_title ( Some ( $ title ) ) ; - $ ( window . set_content ( Some ( $ content ) ) ; ) ? - window -} } } - macro_rules ! pango_attributes { ( - $ ( @ scale : $ scale : expr ; ) ? - $ ( @ weight : $ weight : expr ; ) ? + $ ( scale : $ scale : expr ) ? + $ ( , weight : $ weight : expr $ (,) ? ) ? ) => { { let attributes = gtk4 :: pango :: AttrList :: new () ; # [ allow (unused_mut) ] let mut font_description = gtk4 :: pango :: FontDescription :: new () ; @@ -278,22 +61,6 @@ macro_rules ! pango_attributes { ( # [ allow (unused_imports) ] pub (crate) use { - application_window , - bin , - button , - dialog , - flow_box , - g_box , - header_bar , - image , - label , - list_box , pango_attributes , - popover , - scrolled_window , - split_button , - toolbar_view , - vertically_filling , - view_stack , - view_switcher , + view_expr , } ; From 0a61e55c3ea9c6dfe0b243a21913aa5868f3cb98 Mon Sep 17 00:00:00 2001 From: Reinout Meliesie Date: Fri, 2 Jan 2026 13:41:46 +0100 Subject: [PATCH 2/2] Add editorconfig file --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..54e65d9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.rs] +charset = utf-8 +indent_style = tab +tab_width = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true