├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── rust-toolchain ├── rustfmt.toml └── src ├── functions ├── entries.rs ├── mod.rs ├── settings.rs └── widgets.rs ├── lib.rs ├── macros ├── keybindings.rs └── mod.rs ├── traits ├── dynamic_resize.rs ├── entries_ext.rs └── mod.rs └── widgets_ ├── image_selection.rs ├── mod.rs ├── revealing_button.rs ├── uuid_entry.rs └── variant_toggler.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gtk-extras" 3 | version = "0.3.1" 4 | authors = ["Michael Aaron Murphy "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | cascade = "1.0" 9 | cairo-rs = "0.14" 10 | derive_more = "0.99" 11 | gdk = "0.14" 12 | gio = "0.14" 13 | glib = "0.14" 14 | gtk = { version = "0.14", features = ["v3_22"] } 15 | itertools = "0.9" 16 | log = "0.4" 17 | uuid = "0.8" 18 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 System76 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 System76 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GTK-rs Extras 2 | 3 | A Rust crate containing an assortment of unofficial GTK patterns, widgets, and traits for your GTK Rust projects, curated by the Pop!_OS team. A metaphorical dumping ground for useful functionality for writing GTK applications with. 4 | 5 | If you have any useful generic patterns, widgets, and traits to add to the project, contributions are welcome! 6 | 7 | ## License 8 | 9 | Licensed under either of 10 | 11 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 12 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 13 | 14 | at your option. 15 | 16 | ### Contribution 17 | 18 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 19 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.51.0 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | comment_width = 100 3 | condense_wildcard_suffixes = true 4 | fn_single_line = true 5 | format_strings = true 6 | imports_indent = "Block" 7 | max_width = 100 8 | normalize_comments = true 9 | reorder_imports = true 10 | reorder_modules = true 11 | reorder_impl_items = true 12 | struct_field_align_threshold = 30 13 | use_field_init_shorthand = true 14 | wrap_comments = true 15 | merge_imports = true 16 | force_multiline_blocks = false 17 | use_small_heuristics = "Max" 18 | -------------------------------------------------------------------------------- /src/functions/entries.rs: -------------------------------------------------------------------------------- 1 | //! Functions for interacting with entries. 2 | 3 | use gtk::prelude::*; 4 | use itertools::Itertools; 5 | use std::rc::Rc; 6 | 7 | /// Links multiple entries by triggering a focus grab on an activation. 8 | /// 9 | /// The last entry will activate the `last` closure. 10 | pub fn link<'a, C: Fn(>k::Entry) -> bool + 'static, F: Fn(>k::Entry) + 'static>( 11 | entries: impl Iterator, 12 | condition: C, 13 | last: F, 14 | ) { 15 | let condition = Rc::new(condition); 16 | let mut last_entry = None::; 17 | for (current, next) in entries.tuple_windows() { 18 | let next_ = next.clone(); 19 | let condition = condition.clone(); 20 | current.connect_activate(move |entry| { 21 | if condition(entry) { 22 | next_.grab_focus() 23 | } 24 | }); 25 | last_entry = Some(next); 26 | } 27 | 28 | if let Some(entry) = last_entry { 29 | entry.connect_activate(move |entry| last(entry)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/functions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod entries; 2 | pub mod settings; 3 | pub mod widgets; 4 | -------------------------------------------------------------------------------- /src/functions/settings.rs: -------------------------------------------------------------------------------- 1 | //! Convenience types for interacting with common GSettings parameters. 2 | 3 | use gio::{Settings, SettingsSchemaSource}; 4 | use glib::GString; 5 | use gdk::prelude::*; 6 | 7 | /// Checks if a schema exists before attempting to create a `Settings` for it 8 | /// 9 | /// # Notes 10 | /// 11 | /// This is equivalent to 12 | /// 13 | /// ``` 14 | /// use gtk_extras::settings; 15 | /// 16 | /// let schema = "org.freedesktop.Tracker"; 17 | /// let buf = &mut String::with_capacity(64); 18 | /// if let Some(settings) = settings::new_checked_with_buffer(buf, schema) { 19 | /// println!("settings for {} was found", schema); 20 | /// } 21 | /// ``` 22 | pub fn new_checked(schema: &str) -> Option { 23 | if schema_exists(schema) { 24 | Some(Settings::new(schema)) 25 | } else { 26 | None 27 | } 28 | } 29 | 30 | /// Verifies that a schema exists 31 | /// 32 | /// The default behavior of `GSettings` is to abort a program which tries to access a 33 | /// schema which does not exist. However, this is less than ideal in the real world, 34 | /// where the existing of a schema is entirely optional, so this function provides a 35 | /// means to validate if a schema exists in advance. 36 | /// 37 | /// # Notes 38 | /// 39 | /// This is equivalent to 40 | /// 41 | /// ``` 42 | /// use gtk_extras::settings; 43 | /// 44 | /// let schema = "org.gnome.nautilus"; 45 | /// let buf = &mut String::with_capacity(64); 46 | /// if settings::schema_exists_with_buffer(buf, schema) { 47 | /// println!("settings for {} exists", schema); 48 | /// } 49 | /// ``` 50 | pub fn schema_exists(schema: &str) -> bool { 51 | match SettingsSchemaSource::default() { 52 | Some(source) => source.lookup(schema, true).is_some(), 53 | None => false, 54 | } 55 | } 56 | 57 | /// Convenience type for `org.gnome.gedit.preferences.editor` 58 | pub struct GeditPreferencesEditor(pub Settings); 59 | 60 | impl GeditPreferencesEditor { 61 | pub fn new() -> Self { Self(Settings::new("org.gnome.gedit.preferences.editor")) } 62 | 63 | pub fn new_checked() -> Option { 64 | new_checked("org.gnome.gedit.preferences.editor").map(Self) 65 | } 66 | 67 | /// Get the active scheme 68 | pub fn scheme(&self) -> GString { self.0.string("scheme") } 69 | 70 | /// Set the active scheme 71 | pub fn set_scheme(&self, scheme: &str) { 72 | let _ = self.0.set_string("scheme", scheme); 73 | Settings::sync(); 74 | } 75 | } 76 | 77 | /// Convenience type for `org.gnome.desktop.interface` 78 | pub struct GnomeDesktopInterface(pub Settings); 79 | 80 | impl GnomeDesktopInterface { 81 | pub fn new() -> Self { Self(Settings::new("org.gnome.desktop.interface")) } 82 | 83 | pub fn new_checked() -> Option { 84 | new_checked("org.gnome.desktop.interface").map(Self) 85 | } 86 | 87 | /// Get the active color scheme 88 | pub fn color_scheme(&self) -> GString { self.0.string("color-scheme") } 89 | 90 | /// Set the active color scheme 91 | pub fn set_color_scheme(&self, theme: &str) { 92 | let _ = self.0.set_string("color-scheme", theme); 93 | Settings::sync(); 94 | } 95 | 96 | /// Get the active GTK theme 97 | pub fn gtk_theme(&self) -> GString { self.0.string("gtk-theme") } 98 | 99 | /// Set the active GTK theme 100 | pub fn set_gtk_theme(&self, theme: &str) { 101 | let _ = self.0.set_string("gtk-theme", theme); 102 | Settings::sync(); 103 | } 104 | } 105 | 106 | /// Convenience type for `org.gnome.meld` 107 | pub struct MeldPreferencesEditor(pub Settings); 108 | 109 | impl MeldPreferencesEditor { 110 | pub fn new() -> Self { Self(Settings::new("org.gnome.meld")) } 111 | 112 | pub fn new_checked() -> Option { 113 | new_checked("org.gnome.meld").map(Self) 114 | } 115 | 116 | /// Get the active scheme 117 | pub fn style_scheme(&self) -> GString { self.0.string("style-scheme") } 118 | 119 | /// Set the active scheme 120 | pub fn set_style_scheme(&self, scheme: &str) { 121 | let _ = self.0.set_string("style-scheme", scheme); 122 | Settings::sync(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/functions/widgets.rs: -------------------------------------------------------------------------------- 1 | //! Functions for interacting with widgets. 2 | 3 | use gtk::prelude::*; 4 | 5 | /// Fetches all immediate widgets which are entries in the given container. 6 | pub fn iter_from, C: IsA>( 7 | container: &C, 8 | ) -> impl DoubleEndedIterator { 9 | container.children().into_iter().filter_map(|w| w.downcast::().ok()) 10 | } 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Curated dumping ground for misc. useful GTK widgets and traits. 2 | //! 3 | //! Contains an assortment of unofficial GTK widgets and traits for your GTK Rust projects. 4 | //! 5 | //! [Contributions welcome]()! 6 | 7 | #[macro_use] 8 | extern crate cascade; 9 | #[macro_use] 10 | extern crate derive_more; 11 | #[macro_use] 12 | extern crate log; 13 | 14 | mod functions; 15 | mod macros; 16 | mod traits; 17 | mod widgets_; 18 | 19 | pub use self::{functions::*, macros::*, traits::*, widgets_::*}; 20 | 21 | pub use cascade::cascade; 22 | -------------------------------------------------------------------------------- /src/macros/keybindings.rs: -------------------------------------------------------------------------------- 1 | /// Map key bindings to events in GTK applications 2 | /// 3 | /// This macro creates a `gio::SimpleAction` for each defined event, adds that action to 4 | /// a given `gtk::Application`, and then associates that action with a specified slice of 5 | /// possible key binding accelerators. 6 | /// 7 | /// See [gdk::enums::key](https://gtk-rs.org/docs/gdk/enums/key/index.html) for a list of 8 | /// supported keys. 9 | /// 10 | /// # Example 11 | /// 12 | /// Where `application` is a `>k::Application`, and `sender` is a `&glib::Sender`: 13 | /// 14 | /// ```rust 15 | /// use gio::prelude::*; 16 | /// use gtk::prelude::*; 17 | /// use gtk_extras::keybindings; 18 | /// 19 | /// const APP_ID: &str = "org.Organization.App"; 20 | /// 21 | /// enum UiEvent { 22 | /// Back, 23 | /// Quit, 24 | /// StackNext, 25 | /// StackPrev 26 | /// } 27 | /// 28 | /// let app_flags = gio::ApplicationFlags::empty(); 29 | /// let application = gtk::Application::new(APP_ID.into(), app_flags).unwrap(); 30 | /// 31 | /// application.connect_startup(|app| { 32 | /// let (sender, receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); 33 | /// 34 | /// keybindings!((app, &sender) { 35 | /// "back" => (UiEvent::Back, &["BackSpace"]), 36 | /// "quit" => (UiEvent::Quit, &["Q"]), 37 | /// "stkn" => (UiEvent::StackNext, &["Right"]), 38 | /// "stkp" => (UiEvent::StackPrev, &["Left"]), 39 | /// }); 40 | /// 41 | /// receiver.attach(None, move |event| { 42 | /// match event { 43 | /// UiEvent::Back => (), 44 | /// UiEvent::Quit => { 45 | /// // Destroy main window here. 46 | /// return glib::Continue(false); 47 | /// }, 48 | /// UiEvent::StackNext => (), 49 | /// UiEvent::StackPrev => (), 50 | /// } 51 | /// 52 | /// glib::Continue(true) 53 | /// }); 54 | /// }); 55 | /// 56 | /// application.run(&[]); 57 | /// ``` 58 | #[macro_export] 59 | macro_rules! keybindings { 60 | (($app:expr, $sender:expr) { $( $name:expr => ($value:expr, $keys:expr) ),+ $(,)? }) => ( 61 | use gio::ActionMapExt; 62 | 63 | $({ 64 | let action = gio::SimpleAction::new($name, None); 65 | 66 | let sender = $sender.clone(); 67 | action.connect_activate(move |_, _| { 68 | let _ = sender.send($value); 69 | }); 70 | 71 | $app.add_action(&action); 72 | $app.set_accels_for_action(concat!("app.", $name), $keys); 73 | })+ 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/macros/mod.rs: -------------------------------------------------------------------------------- 1 | mod keybindings; 2 | -------------------------------------------------------------------------------- /src/traits/dynamic_resize.rs: -------------------------------------------------------------------------------- 1 | use core::num::NonZeroU8; 2 | use gtk::prelude::*; 3 | 4 | /// Trait to enable dynamic resizing for a widget, based on another 5 | pub trait DynamicResize 6 | where 7 | Self: WidgetExt, 8 | { 9 | /// When this widget is resized, the `other` widget will also be resized to the given width and 10 | /// height percent (1-100); 11 | /// 12 | /// This is most useful for dynamically resizing the child of a container to be a certain % of 13 | /// the parent's dimensions. 14 | fn dynamic_resize( 15 | &self, 16 | other: W, 17 | width_percent: Option, 18 | height_percent: Option, 19 | ) { 20 | self.connect_size_allocate(move |_, allocation| { 21 | let width = width_percent.map_or(-1, |percent| calc_side(allocation.width, percent)); 22 | let height = height_percent.map_or(-1, |percent| calc_side(allocation.height, percent)); 23 | other.set_size_request(width, height); 24 | }); 25 | } 26 | } 27 | 28 | fn calc_side(measurement: i32, percent: NonZeroU8) -> i32 { 29 | measurement * i32::from(percent.get()) / 100 30 | } 31 | 32 | impl DynamicResize for T {} 33 | -------------------------------------------------------------------------------- /src/traits/entries_ext.rs: -------------------------------------------------------------------------------- 1 | use glib::GString; 2 | use gtk::prelude::*; 3 | 4 | /// Additional methods for interacting with GTK entries 5 | pub trait EntriesExt { 6 | /// Convenience method for `entry.text_length() == 0`. 7 | fn is_empty(&self) -> bool; 8 | 9 | /// Get the text of an entry, or `None` if it is empty. 10 | /// 11 | /// Equivalent to `entry.text().filter(|string| !string.is_empty())` 12 | fn text_nonempty(&self) -> Option; 13 | } 14 | 15 | impl> EntriesExt for T { 16 | fn is_empty(&self) -> bool { self.text_length() == 0 } 17 | 18 | fn text_nonempty(&self) -> Option { 19 | let text = self.text(); 20 | if text.is_empty() { None } else { Some(text) } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/traits/mod.rs: -------------------------------------------------------------------------------- 1 | mod dynamic_resize; 2 | mod entries_ext; 3 | 4 | pub use self::{dynamic_resize::DynamicResize, entries_ext::EntriesExt}; 5 | -------------------------------------------------------------------------------- /src/widgets_/image_selection.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | use std::{collections::HashMap, rc::Rc}; 3 | 4 | /// A list of selections based on radio buttons, with optional images. 5 | #[derive(AsRef, Deref)] 6 | pub struct ImageSelection { 7 | #[as_ref] 8 | #[deref] 9 | container: gtk::FlowBox, 10 | } 11 | 12 | impl ImageSelection { 13 | pub fn new( 14 | variants: &[SelectionVariant], 15 | placeholder: ImageSrc, 16 | event_cb: impl Fn(T) + 'static, 17 | ) -> Self { 18 | let event_cb = Rc::new(event_cb); 19 | 20 | let container = gtk::FlowBoxBuilder::new() 21 | .can_focus(true) 22 | .focus_on_click(false) 23 | .homogeneous(true) 24 | .selection_mode(gtk::SelectionMode::None) 25 | .build(); 26 | 27 | let mut last_radio = None::; 28 | let mut active_radio = None::; 29 | let mut row_association = HashMap::new(); 30 | 31 | for variant in variants { 32 | let event = variant.event; 33 | let event_cb_ = event_cb.clone(); 34 | 35 | let radio = cascade! { 36 | gtk::RadioButton::new(); 37 | ..set_can_focus(false); 38 | ..set_halign(gtk::Align::Center); 39 | ..join_group(last_radio.as_ref()); 40 | ..connect_active_notify(move |_| { 41 | event_cb_(event); 42 | }); 43 | }; 44 | 45 | if variant.active { 46 | active_radio = Some(radio.clone()); 47 | } 48 | 49 | let image_path = variant.image.unwrap_or(placeholder); 50 | 51 | let mut ib = gtk::ImageBuilder::new(); 52 | 53 | match image_path { 54 | ImageSrc::File(path) => ib = ib.file(path), 55 | ImageSrc::Resource(res) => ib = ib.resource(res) 56 | } 57 | 58 | let image = ib.build().upcast::(); 59 | 60 | if let Some((width, height)) = variant.size_request { 61 | image.set_size_request(width, height); 62 | }; 63 | 64 | let widget = cascade! { 65 | gtk::Box::new(gtk::Orientation::Vertical, 12); 66 | ..add(&image); 67 | ..add(>k::LabelBuilder::new().label(variant.name).xalign(0.0).halign(gtk::Align::Center).build()); 68 | ..add(&radio); 69 | }; 70 | 71 | let child = cascade! { 72 | gtk::FlowBoxChild::new(); 73 | ..add(&widget); 74 | }; 75 | 76 | container.add(&child); 77 | 78 | row_association.insert(child, radio.clone()); 79 | 80 | last_radio = Some(radio); 81 | } 82 | 83 | if let Some(radio) = active_radio { 84 | radio.set_active(true); 85 | } 86 | 87 | container.connect_child_activated(move |_, child| { 88 | if let Some(radio) = row_association.get(child) { 89 | radio.set_active(true); 90 | } 91 | }); 92 | 93 | Self { container } 94 | } 95 | } 96 | 97 | #[derive(Clone, Copy)] 98 | pub enum ImageSrc<'a> { 99 | File(&'a str), 100 | Resource(&'a str) 101 | } 102 | 103 | pub struct SelectionVariant<'a, T> { 104 | pub name: &'a str, 105 | pub image: Option>, 106 | pub size_request: Option<(i32, i32)>, 107 | pub active: bool, 108 | pub event: T, 109 | } 110 | -------------------------------------------------------------------------------- /src/widgets_/mod.rs: -------------------------------------------------------------------------------- 1 | mod image_selection; 2 | mod revealing_button; 3 | mod uuid_entry; 4 | mod variant_toggler; 5 | 6 | pub use self::{ 7 | image_selection::{ImageSelection, ImageSrc, SelectionVariant}, 8 | revealing_button::RevealingButton, 9 | uuid_entry::UuidEntry, 10 | variant_toggler::{ToggleVariant, VariantToggler}, 11 | }; 12 | 13 | #[cfg(feature = "svg")] 14 | pub use self::svg_image::SvgImage; 15 | 16 | use gtk::prelude::*; 17 | 18 | /// Inserts a separator as a header between rows in a list box. 19 | fn standard_header(current: >k::ListBoxRow, before: Option<>k::ListBoxRow>) { 20 | if before.is_some() { 21 | current.set_header(Some(>k::Separator::new(gtk::Orientation::Horizontal))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/widgets_/revealing_button.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | 3 | /// A widget which reveals a child widget when clicked 4 | /// 5 | /// The primary widget is displayed at all times, whereas the child widget is generated 6 | /// on the first reveal. 7 | #[derive(AsRef, Deref)] 8 | pub struct RevealingButton { 9 | #[as_ref] 10 | #[deref] 11 | container: gtk::Container, 12 | 13 | _dropdown_image: gtk::Image, 14 | 15 | pub event_box: gtk::EventBox, 16 | pub revealer: gtk::Revealer, 17 | } 18 | 19 | impl RevealingButton { 20 | pub fn new(main_content: M) -> Self 21 | where 22 | M: FnOnce(>k::Image) -> gtk::Widget, 23 | { 24 | let dropdown_image = gtk::ImageBuilder::new() 25 | .icon_name("pan-end-symbolic") 26 | .icon_size(gtk::IconSize::Menu.into()) 27 | .halign(gtk::Align::Start) 28 | .valign(gtk::Align::Center) 29 | .build(); 30 | 31 | let dropdown_image_ = dropdown_image.downgrade(); 32 | let revealer = cascade! { 33 | gtk::Revealer::new(); 34 | ..connect_reveal_child_notify(move |revealer| { 35 | dropdown_image_.upgrade() 36 | .expect("dropdown image did not exist") 37 | .set_from_icon_name( 38 | Some(if revealer.reveals_child() { 39 | "pan-down-symbolic" 40 | } else { 41 | "pan-end-symbolic" 42 | }), 43 | gtk::IconSize::Menu 44 | ); 45 | }); 46 | }; 47 | 48 | let event_box = cascade! { 49 | gtk::EventBoxBuilder::new() 50 | .can_focus(false) 51 | .hexpand(true) 52 | .events(gdk::EventMask::BUTTON_PRESS_MASK) 53 | .build(); 54 | ..add(&main_content(&dropdown_image)); 55 | }; 56 | 57 | let container = cascade! { 58 | gtk::Box::new(gtk::Orientation::Vertical, 4); 59 | ..set_border_width(12); 60 | ..set_can_focus(false); 61 | ..add(&event_box); 62 | ..add(&revealer); 63 | }; 64 | 65 | Self { 66 | container: container.upcast::(), 67 | _dropdown_image: dropdown_image, 68 | event_box, 69 | revealer, 70 | } 71 | } 72 | 73 | /// Activates when the widget's container is clicked. 74 | pub fn connect_clicked(&self, func: F) { 75 | let revealer = self.revealer.downgrade(); 76 | self.event_box.connect_button_press_event(move |_, _| { 77 | func(revealer.upgrade().expect("revealer for device did not exist")); 78 | gtk::Inhibit(true) 79 | }); 80 | } 81 | 82 | /// Reveals an inner child, and generates it if it is missing. 83 | pub fn reveal gtk::Widget>(&self, mut func: F) -> bool { 84 | let reveal = if self.revealer.reveals_child() { 85 | false 86 | } else { 87 | if self.revealer.child().is_none() { 88 | self.revealer.add(&func()); 89 | } 90 | 91 | true 92 | }; 93 | 94 | self.revealer.set_reveal_child(reveal); 95 | reveal 96 | } 97 | 98 | /// Defines the revealed status of the child widget. 99 | pub fn set_reveal_child(&self, reveal: bool) { self.revealer.set_reveal_child(reveal); } 100 | 101 | /// If the button has already generated a child widget, destroy it. 102 | pub fn destroy_revealed(&self) { 103 | if let Some(child) = self.revealer.child() { 104 | unsafe { child.destroy() } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/widgets_/uuid_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::EntriesExt; 2 | use gtk::prelude::*; 3 | use std::{cell::RefCell, rc::Rc, time::Duration}; 4 | use uuid::Uuid; 5 | 6 | /// Variant of an Entry for handling UUID inputs 7 | /// 8 | /// When inputs are given to this entry, the input will be cleared if it does 9 | /// not contain a valid UUID value after the allotted timeout value has passed 10 | /// since the last input into the entry. 11 | /// 12 | /// # Use Case 13 | /// 14 | /// System76 uses this widget for an internal project which involves scanning 15 | /// bar codes into entries, which the scanner translates into a string 16 | /// representation of a UUID. 17 | /// 18 | /// To reduce the chance of human error, entries will clear their fields when 19 | /// they contain invalid inputs. However, because scanners input one character 20 | /// at a time into the entry, a timeout is necessary to wait for the scanner to 21 | /// complete its input. 22 | /// 23 | /// Furthermore, once a UUID has been submitted, the scanner sends the return 24 | /// key, which activates the entry, submits the UUID to be handled in another 25 | /// process, and clears the entry so that the user can scan the next bar code. 26 | /// 27 | /// # Examples 28 | /// 29 | /// ```rust 30 | /// use gtk_extras::UuidEntry; 31 | /// use uuid::Uuid; 32 | /// 33 | /// enum UiEvent { 34 | /// Received(Uuid) 35 | /// } 36 | /// 37 | /// gtk::init(); 38 | /// 39 | /// let (sender, receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); 40 | /// 41 | /// let entry = UuidEntry::new(1000); 42 | /// let sender = sender.clone(); 43 | /// entry.connect_activate(move |entry| { 44 | /// if let Some(uuid) = entry.get_uuid() { 45 | /// let _ = sender.send(UiEvent::Received(uuid)); 46 | /// } 47 | /// }); 48 | /// 49 | /// receiver.attach(None, move |event| { 50 | /// match event { 51 | /// UiEvent::Received(uuid) => { 52 | /// println!("received {}", uuid); 53 | /// } 54 | /// } 55 | /// 56 | /// glib::Continue(true) 57 | /// }); 58 | /// ``` 59 | #[derive(AsRef, Deref)] 60 | #[as_ref] 61 | #[deref] 62 | pub struct UuidEntry(gtk::Entry); 63 | 64 | impl UuidEntry { 65 | pub fn new(timeout: u32) -> Self { 66 | let entry = gtk::Entry::new(); 67 | let source = Rc::new(RefCell::new(None)); 68 | 69 | entry.connect_changed(move |entry| { 70 | // Ignore the change if the change was to set the entry to an empty string. 71 | if entry.is_empty() { 72 | return; 73 | } 74 | 75 | let entry = entry.clone(); 76 | let source_ = source.clone(); 77 | 78 | let mut source = source.borrow_mut(); 79 | if let Some(source) = source.take() { 80 | glib::source_remove(source); 81 | } 82 | 83 | *source = Some(glib::timeout_add_local(Duration::from_millis(timeout.into()), move || { 84 | let text = entry.text(); 85 | if text.parse::().is_err() { 86 | error!("{} is not a valid UUID", text); 87 | entry.set_text(""); 88 | } 89 | 90 | *source_.borrow_mut() = None; 91 | glib::Continue(false) 92 | })); 93 | }); 94 | 95 | Self(entry) 96 | } 97 | 98 | pub fn connect_activate(&self, f: F) -> glib::SignalHandlerId { 99 | self.0.connect_activate(move |e| f(&Self(e.clone()))) 100 | } 101 | 102 | /// Fetches the UUID, and clears the contents of the entry. 103 | pub fn get_uuid(&self) -> Option { 104 | let text = self.text(); 105 | self.set_text(""); 106 | text.parse::().ok() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/widgets_/variant_toggler.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | use std::rc::Rc; 3 | 4 | /// A list box containing a collection of toggleable variants. 5 | #[derive(AsRef, Deref)] 6 | pub struct VariantToggler { 7 | #[as_ref] 8 | #[deref] 9 | container: gtk::Container, 10 | } 11 | 12 | impl VariantToggler { 13 | pub fn new( 14 | variants: &[ToggleVariant], 15 | event_cb: impl Fn(T, bool) + 'static, 16 | ) -> Self { 17 | let event_cb = Rc::new(event_cb); 18 | 19 | let container = gtk::ListBoxBuilder::new().selection_mode(gtk::SelectionMode::None).build(); 20 | 21 | container.set_header_func(Some(Box::new(super::standard_header))); 22 | 23 | for variant in variants { 24 | let switch = gtk::SwitchBuilder::new() 25 | .halign(gtk::Align::End) 26 | .valign(gtk::Align::Center) 27 | .active(variant.active) 28 | .build(); 29 | 30 | let event = variant.event; 31 | let event_cb_ = event_cb.clone(); 32 | switch.connect_changed_active(move |switch| { 33 | event_cb_(event, switch.is_active()); 34 | }); 35 | 36 | let switch = switch.upcast::(); 37 | 38 | let title_label = gtk::LabelBuilder::new() 39 | .label(variant.name) 40 | .hexpand(true) 41 | .xalign(0.0) 42 | .use_underline(true) 43 | .mnemonic_widget(&switch) 44 | .build(); 45 | 46 | let desc_label = 47 | gtk::LabelBuilder::new().xalign(0.0).label(variant.description).build(); 48 | 49 | desc_label.style_context().add_class(>k::STYLE_CLASS_DIM_LABEL); 50 | 51 | let variant_container = gtk::GridBuilder::new() 52 | .row_spacing(2) 53 | .column_spacing(16) 54 | .margin_start(20) 55 | .margin_end(20) 56 | .margin_top(6) 57 | .margin_bottom(6) 58 | .valign(gtk::Align::Center) 59 | .build(); 60 | 61 | variant_container.attach(&title_label, 0, 0, 1, 1); 62 | variant_container.attach(&desc_label, 0, 1, 1, 1); 63 | variant_container.attach(&switch, 1, 0, 1, 2); 64 | 65 | container.add(&variant_container); 66 | } 67 | 68 | Self { container: container.upcast::() } 69 | } 70 | } 71 | 72 | impl Into for VariantToggler { 73 | fn into(self) -> gtk::Container { self.container } 74 | } 75 | /// A variant for the `VariantToggler` widget. 76 | pub struct ToggleVariant<'a, T> { 77 | pub name: &'a str, 78 | pub description: &'a str, 79 | pub active: bool, 80 | pub event: T, 81 | } 82 | --------------------------------------------------------------------------------