├── media ├── modal.png └── dialog.png ├── src ├── lib.rs └── modal.rs ├── .gitignore ├── Cargo.toml ├── CHANGELOG.md ├── LICENSE ├── README.md └── examples └── example.rs /media/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00kii/egui-modal/HEAD/media/modal.png -------------------------------------------------------------------------------- /media/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00kii/egui-modal/HEAD/media/dialog.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | //! egui-modal 3 | //! Modal library for [`egui`] 4 | mod modal; 5 | pub use modal::*; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | # Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui-modal" 3 | version = "0.6.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "a modal library for egui" 7 | repository = "https://github.com/n00kii/egui-modal" 8 | readme = "README.md" 9 | authors = ["n00kii"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | egui = { version = "0.30.0", default-features = false } 15 | 16 | [dev-dependencies] 17 | eframe = "0.30.0" 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.6.0 2 | - update `egui` -> `0.30` 3 | 4 | # 0.5.0 5 | - update `egui` -> `0.29` 6 | 7 | # 0.4.0 8 | - update `egui` -> `0.28` 9 | 10 | # 0.3.6 11 | - update `egui` -> `0.27` 12 | 13 | # 0.3.5 14 | - add deadlock warning to some functions 15 | - update `egui` -> `0.26.2` 16 | 17 | # 0.3.4 18 | - disable default features for `egui` 19 | - update `egui` -> `0.26.1` 20 | 21 | # 0.3.3 22 | - update `egui` -> `0.26.0` 23 | 24 | # 0.3.2 25 | - update `egui` -> `0.25.0` 26 | 27 | # 0.3.1 28 | - update `egui` -> `0.24.1` 29 | 30 | # 0.3.0 31 | - deprecate `Modal::open_dialog` (use `Modal::dialog`, `DialogBuilder::with_*` and `DialogBuilder::show` functions instead) 32 | - update `egui` -> `0.24.0` 33 | - clippy fixes 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 n00kii 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 | # egui-modal, a modal library for [`egui`](https://github.com/emilk/egui) 2 | [![crates.io](https://img.shields.io/crates/v/egui-modal)](https://crates.io/crates/egui-modal) 3 | [![docs](https://docs.rs/egui-modal/badge.svg)](https://docs.rs/egui-modal/latest/egui_modal/) 4 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/n00kii/egui-modal/blob/main/README.md) 5 | 6 | ![modal](https://raw.githubusercontent.com/n00kii/egui-modal/main/media/modal.png?token=GHSAT0AAAAAABVWXBGJBQSFC3PLQP4KKOG6YZJIDCA) 7 | 8 | ## normal usage: 9 | ```rust 10 | /* calling every frame */ 11 | 12 | let modal = Modal::new(ctx, "my_modal"); 13 | 14 | // What goes inside the modal 15 | modal.show(|ui| { 16 | // these helper functions help set the ui based on the modal's 17 | // set style, but they are not required and you can put whatever 18 | // ui you want inside [`.show()`] 19 | modal.title(ui, "Hello world!"); 20 | modal.frame(ui, |ui| { 21 | modal.body(ui, "This is a modal."); 22 | }); 23 | modal.buttons(ui, |ui| { 24 | // After clicking, the modal is automatically closed 25 | if modal.button(ui, "close").clicked() { 26 | println!("Hello world!") 27 | }; 28 | }); 29 | }); 30 | 31 | if ui.button("Open the modal").clicked() { 32 | // Show the modal 33 | modal.open(); 34 | } 35 | ``` 36 | ## dialog usage 37 | ![dialog](https://raw.githubusercontent.com/n00kii/egui-modal/main/media/dialog.png) 38 | 39 | in some use cases, it may be more convenient to both open and style the modal as a dialog as a one-time action, like on the single instance of a function's return. 40 | ```rust 41 | /* calling every frame */ 42 | 43 | let modal = Modal::new(ctx, "my_dialog"); 44 | 45 | ... 46 | ... 47 | ... 48 | 49 | // Show the dialog 50 | modal.show_dialog(); 51 | ``` 52 | elsewhere, 53 | ```rust 54 | /* happens once */ 55 | if let Ok(data) = my_function() { 56 | modal.dialog() 57 | .with_title("my_function's result is...") 58 | .with_body("my_function was successful!") 59 | .with_icon(Icon::Success) 60 | .open() 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /examples/example.rs: -------------------------------------------------------------------------------- 1 | use egui::{self, DragValue}; 2 | use egui_modal::{Icon, Modal, ModalStyle}; 3 | 4 | struct ExampleApp { 5 | modal_style: ModalStyle, 6 | modal_title: String, 7 | modal_body: String, 8 | nested_modal_text: String, 9 | 10 | include_title: bool, 11 | include_body: bool, 12 | include_buttons: bool, 13 | close_on_outside_click: bool, 14 | 15 | dialog_icon: Option, 16 | } 17 | 18 | impl Default for ExampleApp { 19 | fn default() -> Self { 20 | Self { 21 | modal_style: ModalStyle::default(), 22 | modal_title: "a modal".to_string(), 23 | modal_body: "here is the modal body".to_string(), 24 | 25 | nested_modal_text: String::new(), 26 | include_title: true, 27 | include_body: true, 28 | include_buttons: true, 29 | close_on_outside_click: false, 30 | 31 | dialog_icon: Some(Icon::Info), 32 | } 33 | } 34 | } 35 | 36 | impl eframe::App for ExampleApp { 37 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 38 | egui::Window::new("egui-modal").show(ctx, |ui| { 39 | // you can put the modal creation and show logic wherever you want 40 | // (though of course it needs to be created before it can be used) 41 | let nested_modal = Modal::new(ctx, "nested_modal"); 42 | let modal = Modal::new(ctx, "modal") 43 | .with_style(&self.modal_style) 44 | .with_close_on_outside_click(self.close_on_outside_click || !self.include_buttons); 45 | 46 | // the show function defines what is shown in the modal, but the modal 47 | // won't actually show until you do modal.open() 48 | modal.show(|ui| { 49 | // these helper functions are NOT mandatory to use, they just 50 | // help implement some styling with margins and separators 51 | // you can put whatever you like in here 52 | if self.include_title { 53 | modal.title(ui, &mut self.modal_title); 54 | } 55 | // the "frame" of the modal refers to the container of the icon and body. 56 | // this helper just applies a margin specified by the ModalStyle 57 | modal.frame(ui, |ui| { 58 | if self.include_body { 59 | modal.body(ui, &self.modal_body); 60 | } 61 | }); 62 | if self.include_buttons { 63 | modal.buttons(ui, |ui| { 64 | if modal.button(ui, "close").clicked() 65 | || (self.close_on_outside_click && modal.was_outside_clicked()) 66 | { 67 | // all buttons created with the helper functions automatically 68 | // close the modal on click, but you can close it yourself with 69 | // ['modal.close()'] 70 | println!("hello world!") 71 | } 72 | 73 | modal.caution_button(ui, "button, but caution"); 74 | if modal.suggested_button(ui, "open another modal").clicked() { 75 | // always close your previous modal before opening a new one otherwise weird 76 | // layering things will happen. again, the helper functions for the buttons automatically 77 | // close the modal on click, so we don't have to manually do that here 78 | nested_modal.open(); 79 | } 80 | }) 81 | } 82 | }); 83 | 84 | // a dialog is useful when you have a one-time occurance and you want to relay information to the user 85 | let mut dialog_modal = Modal::new(ctx, "dialog_modal").with_style(&self.modal_style); 86 | // make sure you don't forget to show the dialog! 87 | dialog_modal.show_dialog(); 88 | 89 | ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui| { 90 | if ui.button("open modal").clicked() { 91 | modal.open(); 92 | } 93 | 94 | if ui.button("open dialog").clicked() { 95 | // [`.dialog()`] can be used to both set the visual info for the dialog 96 | // and open it at the same time 97 | let mut dialog_builder = dialog_modal 98 | .dialog() 99 | .with_title("this is a dialog") 100 | .with_body("this helps for showing information about one-time events"); 101 | if let Some(dialog_icon) = self.dialog_icon.clone() { 102 | dialog_builder = dialog_builder.with_icon(dialog_icon); 103 | } 104 | dialog_builder.open(); 105 | } 106 | 107 | ui.separator(); 108 | // to prevent locking the example window without any way to close the modal :) 109 | // remember to implement this yourself if you don't use buttons in your modal 110 | let mut cooc_enabled = self.close_on_outside_click || !self.include_buttons; 111 | ui.add_enabled_ui(self.include_buttons, |ui| { 112 | if ui 113 | .checkbox(&mut cooc_enabled, "close if click outside modal") 114 | .clicked() 115 | { 116 | self.close_on_outside_click = !self.close_on_outside_click 117 | }; 118 | }); 119 | ui.checkbox(&mut self.include_title, "include title"); 120 | ui.checkbox(&mut self.include_body, "include body"); 121 | ui.checkbox(&mut self.include_buttons, "include buttons"); 122 | ui.separator(); 123 | egui::Grid::new("options_grid") 124 | .min_col_width(200.) 125 | .striped(true) 126 | .show(ui, |ui| { 127 | ui.label("title"); 128 | ui.text_edit_singleline(&mut self.modal_title); 129 | ui.end_row(); 130 | 131 | ui.label("body"); 132 | ui.text_edit_singleline(&mut self.modal_body); 133 | ui.end_row(); 134 | 135 | let mut has_height = self.modal_style.default_height.is_some(); 136 | let mut has_width = self.modal_style.default_width.is_some(); 137 | if ui.checkbox(&mut has_height, "default height").changed() { 138 | if has_height { 139 | self.modal_style.default_height = Some(100.) 140 | } else { 141 | self.modal_style.default_height = None 142 | } 143 | } 144 | if let Some(modal_height) = self.modal_style.default_height.as_mut() { 145 | let modal_height = DragValue::new(modal_height).range(0..=1000); 146 | ui.add_sized(ui.available_rect_before_wrap().size(), modal_height); 147 | } 148 | ui.end_row(); 149 | 150 | if ui.checkbox(&mut has_width, "default width").changed() { 151 | if has_width { 152 | self.modal_style.default_width = Some(100.) 153 | } else { 154 | self.modal_style.default_width = None 155 | } 156 | } 157 | if let Some(modal_width) = self.modal_style.default_width.as_mut() { 158 | let modal_width = DragValue::new(modal_width).range(0..=1000); 159 | ui.add_sized(ui.available_rect_before_wrap().size(), modal_width); 160 | } 161 | ui.end_row(); 162 | 163 | ui.label("body margin"); 164 | let body_margin = 165 | DragValue::new(&mut self.modal_style.body_margin).range(0..=20); 166 | ui.add_sized(ui.available_rect_before_wrap().size(), body_margin); 167 | ui.end_row(); 168 | 169 | ui.label("frame margin"); 170 | let frame_margin = 171 | DragValue::new(&mut self.modal_style.frame_margin).range(0..=20); 172 | ui.add_sized(ui.available_rect_before_wrap().size(), frame_margin); 173 | ui.end_row(); 174 | 175 | ui.label("icon margin"); 176 | let icon_margin = 177 | DragValue::new(&mut self.modal_style.icon_margin).range(0..=20); 178 | ui.add_sized(ui.available_rect_before_wrap().size(), icon_margin); 179 | ui.end_row(); 180 | 181 | ui.label("icon size"); 182 | let icon_size = 183 | DragValue::new(&mut self.modal_style.icon_size).range(8..=48); 184 | ui.add_sized(ui.available_rect_before_wrap().size(), icon_size); 185 | ui.end_row(); 186 | 187 | ui.label("dialog icon"); 188 | let mut use_icon = self.dialog_icon.is_some(); 189 | ui.horizontal(|ui| { 190 | if ui.checkbox(&mut use_icon, "use a dialog icon").clicked() { 191 | if use_icon { 192 | self.dialog_icon = Some(Icon::Info); 193 | } else { 194 | self.dialog_icon = None; 195 | } 196 | } 197 | if let Some(icon) = self.dialog_icon.as_mut() { 198 | ui.selectable_value(icon, Icon::Info, "info"); 199 | ui.selectable_value(icon, Icon::Warning, "warning"); 200 | ui.selectable_value(icon, Icon::Success, "success"); 201 | ui.selectable_value(icon, Icon::Error, "error"); 202 | } 203 | }); 204 | ui.end_row(); 205 | 206 | ui.label("body alignment"); 207 | ui.horizontal(|ui| { 208 | ui.selectable_value( 209 | &mut self.modal_style.body_alignment, 210 | egui::Align::Min, 211 | "min", 212 | ); 213 | ui.selectable_value( 214 | &mut self.modal_style.body_alignment, 215 | egui::Align::Center, 216 | "center", 217 | ); 218 | ui.selectable_value( 219 | &mut self.modal_style.body_alignment, 220 | egui::Align::Max, 221 | "max", 222 | ); 223 | }); 224 | ui.end_row(); 225 | 226 | ui.label("overlay color"); 227 | ui.color_edit_button_srgba(&mut self.modal_style.overlay_color); 228 | ui.end_row(); 229 | 230 | ui.label("caution button (fill, text)"); 231 | ui.horizontal(|ui| { 232 | ui.color_edit_button_srgba(&mut self.modal_style.caution_button_fill); 233 | ui.color_edit_button_srgba( 234 | &mut self.modal_style.caution_button_text_color, 235 | ); 236 | }); 237 | ui.end_row(); 238 | 239 | ui.label("suggested button (fill, text)"); 240 | ui.horizontal(|ui| { 241 | ui.color_edit_button_srgba(&mut self.modal_style.suggested_button_fill); 242 | ui.color_edit_button_srgba( 243 | &mut self.modal_style.suggested_button_text_color, 244 | ); 245 | }); 246 | ui.end_row(); 247 | 248 | ui.label("icon colors (info, warning, success, error)"); 249 | ui.horizontal(|ui| { 250 | ui.color_edit_button_srgba(&mut self.modal_style.info_icon_color); 251 | ui.color_edit_button_srgba(&mut self.modal_style.warning_icon_color); 252 | ui.color_edit_button_srgba(&mut self.modal_style.success_icon_color); 253 | ui.color_edit_button_srgba(&mut self.modal_style.error_icon_color); 254 | }); 255 | ui.end_row(); 256 | }); 257 | }); 258 | 259 | // why is this down here?? just wanted to show that you can put 260 | // the modal's [`.show()`] anywhere but we could have put this above 261 | // modal if we wanted 262 | nested_modal.show(|ui| { 263 | nested_modal.frame(ui, |ui| { 264 | nested_modal.body(ui, "hello there!"); 265 | // you can put textboxes in here. 266 | ui.text_edit_singleline(&mut self.nested_modal_text); 267 | }); 268 | nested_modal.buttons(ui, |ui| { 269 | nested_modal.button(ui, "close"); 270 | }) 271 | }); 272 | }); 273 | } 274 | } 275 | fn main() { 276 | let _ = eframe::run_native( 277 | "egui-modal example", 278 | eframe::NativeOptions::default(), 279 | Box::new(|_cc| Ok(Box::new(ExampleApp::default()))), 280 | ); 281 | } 282 | -------------------------------------------------------------------------------- /src/modal.rs: -------------------------------------------------------------------------------- 1 | use egui::{ 2 | emath::{Align, Align2}, 3 | epaint::{Color32, Pos2, Rounding}, 4 | Area, Button, Context, Id, Layout, Response, RichText, Sense, Ui, WidgetText, Window, 5 | }; 6 | 7 | const ERROR_ICON_COLOR: Color32 = Color32::from_rgb(200, 90, 90); 8 | const INFO_ICON_COLOR: Color32 = Color32::from_rgb(150, 200, 210); 9 | const WARNING_ICON_COLOR: Color32 = Color32::from_rgb(230, 220, 140); 10 | const SUCCESS_ICON_COLOR: Color32 = Color32::from_rgb(140, 230, 140); 11 | 12 | const CAUTION_BUTTON_FILL: Color32 = Color32::from_rgb(87, 38, 34); 13 | const SUGGESTED_BUTTON_FILL: Color32 = Color32::from_rgb(33, 54, 84); 14 | const CAUTION_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(242, 148, 148); 15 | const SUGGESTED_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(141, 182, 242); 16 | 17 | const OVERLAY_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 200); 18 | 19 | /// The different styles a modal button can take. 20 | pub enum ModalButtonStyle { 21 | /// A normal [`egui`] button 22 | None, 23 | /// A button highlighted blue 24 | Suggested, 25 | /// A button highlighted red 26 | Caution, 27 | } 28 | 29 | /// An icon. If used, it will be shown next to the body of 30 | /// the modal. 31 | #[derive(Clone, Default, PartialEq)] 32 | pub enum Icon { 33 | #[default] 34 | /// An info icon 35 | Info, 36 | /// A warning icon 37 | Warning, 38 | /// A success icon 39 | Success, 40 | /// An error icon 41 | Error, 42 | /// A custom icon. The first field in the tuple is 43 | /// the text of the icon, and the second field is the 44 | /// color. 45 | Custom((String, Color32)), 46 | } 47 | 48 | impl std::fmt::Display for Icon { 49 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 50 | match self { 51 | Icon::Info => write!(f, "ℹ"), 52 | Icon::Warning => write!(f, "⚠"), 53 | Icon::Success => write!(f, "✔"), 54 | Icon::Error => write!(f, "❗"), 55 | Icon::Custom((icon_text, _)) => write!(f, "{icon_text}"), 56 | } 57 | } 58 | } 59 | 60 | #[derive(Clone, Default)] 61 | struct DialogData { 62 | title: Option, 63 | body: Option, 64 | icon: Option, 65 | } 66 | 67 | /// Used for constructing and opening a modal dialog. This can be used 68 | /// to both set the title/body/icon of the modal and open it as a one-time call 69 | /// (as opposed to a continous call in the update loop) at the same time. 70 | /// Make sure to call `DialogBuilder::open` to actually open the dialog. 71 | #[must_use = "use `DialogBuilder::open`"] 72 | pub struct DialogBuilder { 73 | data: DialogData, 74 | modal_id: Id, 75 | ctx: Context, 76 | } 77 | 78 | #[derive(Clone)] 79 | enum ModalType { 80 | Modal, 81 | Dialog(DialogData), 82 | } 83 | 84 | #[derive(Clone)] 85 | /// Information about the current state of the modal. (Pretty empty 86 | /// right now but may be expanded upon in the future.) 87 | struct ModalState { 88 | is_open: bool, 89 | was_outside_clicked: bool, 90 | modal_type: ModalType, 91 | last_frame_height: Option, 92 | } 93 | 94 | #[derive(Clone, Debug)] 95 | /// Contains styling parameters for the modal, like body margin 96 | /// and button colors. 97 | pub struct ModalStyle { 98 | /// The margin around the modal body. Only applies if using 99 | /// [`.body()`] 100 | pub body_margin: f32, 101 | /// The margin around the container of the icon and body. Only 102 | /// applies if using [`.frame()`] 103 | pub frame_margin: f32, 104 | /// The margin around the container of the icon. Only applies 105 | /// if using [`.icon()`]. 106 | pub icon_margin: f32, 107 | /// The size of any icons used in the modal 108 | pub icon_size: f32, 109 | /// The color of the overlay that dims the background 110 | pub overlay_color: Color32, 111 | 112 | /// The fill color for the caution button style 113 | pub caution_button_fill: Color32, 114 | /// The fill color for the suggested button style 115 | pub suggested_button_fill: Color32, 116 | 117 | /// The text color for the caution button style 118 | pub caution_button_text_color: Color32, 119 | /// The text color for the suggested button style 120 | pub suggested_button_text_color: Color32, 121 | 122 | /// The text of the acknowledgement button for dialogs 123 | pub dialog_ok_text: String, 124 | 125 | /// The color of the info icon 126 | pub info_icon_color: Color32, 127 | /// The color of the warning icon 128 | pub warning_icon_color: Color32, 129 | /// The color of the success icon 130 | pub success_icon_color: Color32, 131 | /// The color of the error icon 132 | pub error_icon_color: Color32, 133 | 134 | /// The default width of the modal 135 | pub default_width: Option, 136 | /// The default height of the modal 137 | pub default_height: Option, 138 | 139 | /// The alignment of text inside the body 140 | pub body_alignment: Align, 141 | } 142 | 143 | impl ModalState { 144 | fn load(ctx: &Context, id: Id) -> Self { 145 | ctx.data_mut(|d| d.get_temp(id).unwrap_or_default()) 146 | } 147 | fn save(self, ctx: &Context, id: Id) { 148 | ctx.data_mut(|d| d.insert_temp(id, self)) 149 | } 150 | } 151 | 152 | impl Default for ModalState { 153 | fn default() -> Self { 154 | Self { 155 | was_outside_clicked: false, 156 | is_open: false, 157 | modal_type: ModalType::Modal, 158 | last_frame_height: None, 159 | } 160 | } 161 | } 162 | 163 | impl Default for ModalStyle { 164 | fn default() -> Self { 165 | Self { 166 | body_margin: 5., 167 | icon_margin: 7., 168 | frame_margin: 2., 169 | icon_size: 30., 170 | overlay_color: OVERLAY_COLOR, 171 | 172 | caution_button_fill: CAUTION_BUTTON_FILL, 173 | suggested_button_fill: SUGGESTED_BUTTON_FILL, 174 | 175 | caution_button_text_color: CAUTION_BUTTON_TEXT_COLOR, 176 | suggested_button_text_color: SUGGESTED_BUTTON_TEXT_COLOR, 177 | 178 | dialog_ok_text: "ok".to_string(), 179 | 180 | info_icon_color: INFO_ICON_COLOR, 181 | warning_icon_color: WARNING_ICON_COLOR, 182 | success_icon_color: SUCCESS_ICON_COLOR, 183 | error_icon_color: ERROR_ICON_COLOR, 184 | 185 | default_height: None, 186 | default_width: None, 187 | 188 | body_alignment: Align::Min, 189 | } 190 | } 191 | } 192 | /// A [`Modal`] is created using [`Modal::new()`]. Make sure to use a `let` binding when 193 | /// using [`Modal::new()`] to ensure you can call things like [`Modal::open()`] later on. 194 | /// ``` 195 | /// let modal = Modal::new(ctx, "my_modal"); 196 | /// modal.show(|ui| { 197 | /// ui.label("Hello world!") 198 | /// }); 199 | /// if ui.button("modal").clicked() { 200 | /// modal.open(); 201 | /// } 202 | /// ``` 203 | /// Helper functions are also available to use that help apply margins based on the modal's 204 | /// [`ModalStyle`]. They are not necessary to use, but may help reduce boilerplate. 205 | /// ``` 206 | /// let other_modal = Modal::new(ctx, "another_modal"); 207 | /// other_modal.show(|ui| { 208 | /// other_modal.frame(ui, |ui| { 209 | /// other_modal.body(ui, "Hello again, world!"); 210 | /// }); 211 | /// other_modal.buttons(ui, |ui| { 212 | /// other_modal.button(ui, "Close"); 213 | /// }); 214 | /// }); 215 | /// if ui.button("open the other modal").clicked() { 216 | /// other_modal.open(); 217 | /// } 218 | /// ``` 219 | pub struct Modal { 220 | close_on_outside_click: bool, 221 | style: ModalStyle, 222 | ctx: Context, 223 | id: Id, 224 | window_id: Id, 225 | } 226 | 227 | fn ui_with_margin(ui: &mut Ui, margin: f32, add_contents: impl FnOnce(&mut Ui) -> R) { 228 | egui::Frame::none() 229 | .inner_margin(margin) 230 | .show(ui, |ui| add_contents(ui)); 231 | } 232 | 233 | impl Modal { 234 | /// Creates a new [`Modal`]. Can use constructor functions like [`Modal::with_style`] 235 | /// to modify upon creation. 236 | pub fn new(ctx: &Context, id_source: impl std::fmt::Display) -> Self { 237 | let self_id = Id::new(id_source.to_string()); 238 | Self { 239 | window_id: self_id.with("window"), 240 | id: self_id, 241 | style: ModalStyle::default(), 242 | ctx: ctx.clone(), 243 | close_on_outside_click: false, 244 | } 245 | } 246 | 247 | fn set_open_state(&self, is_open: bool) { 248 | let mut modal_state = ModalState::load(&self.ctx, self.id); 249 | modal_state.is_open = is_open; 250 | modal_state.save(&self.ctx, self.id) 251 | } 252 | 253 | fn set_outside_clicked(&self, was_clicked: bool) { 254 | let mut modal_state = ModalState::load(&self.ctx, self.id); 255 | modal_state.was_outside_clicked = was_clicked; 256 | modal_state.save(&self.ctx, self.id) 257 | } 258 | 259 | /// Was the outer overlay clicked this frame? 260 | pub fn was_outside_clicked(&self) -> bool { 261 | let modal_state = ModalState::load(&self.ctx, self.id); 262 | modal_state.was_outside_clicked 263 | } 264 | 265 | /// Is the modal currently open? 266 | pub fn is_open(&self) -> bool { 267 | let modal_state = ModalState::load(&self.ctx, self.id); 268 | modal_state.is_open 269 | } 270 | 271 | /// Open the modal; make it visible. The modal prevents user input to other parts of the 272 | /// application. 273 | /// 274 | /// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within 275 | /// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15) 276 | pub fn open(&self) { 277 | self.set_open_state(true) 278 | } 279 | 280 | /// Close the modal so that it is no longer visible, allowing input to flow back into 281 | /// the application. 282 | /// 283 | /// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within 284 | /// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15) 285 | pub fn close(&self) { 286 | self.set_open_state(false) 287 | } 288 | 289 | /// If set to `true`, the modal will close itself if the user clicks outside on the modal window 290 | /// (onto the overlay). 291 | pub fn with_close_on_outside_click(mut self, do_close_on_click_ouside: bool) -> Self { 292 | self.close_on_outside_click = do_close_on_click_ouside; 293 | self 294 | } 295 | 296 | /// Change the [`ModalStyle`] of the modal upon creation. 297 | pub fn with_style(mut self, style: &ModalStyle) -> Self { 298 | self.style = style.clone(); 299 | self 300 | } 301 | 302 | /// Helper function for styling the title of the modal. 303 | /// ``` 304 | /// let modal = Modal::new(ctx, "modal"); 305 | /// modal.show(|ui| { 306 | /// modal.title(ui, "my title"); 307 | /// }); 308 | /// ``` 309 | pub fn title(&self, ui: &mut Ui, text: impl Into) { 310 | let text: RichText = text.into(); 311 | ui.vertical_centered(|ui| { 312 | ui.heading(text); 313 | }); 314 | ui.separator(); 315 | } 316 | 317 | /// Helper function for styling the icon of the modal. 318 | /// ``` 319 | /// let modal = Modal::new(ctx, "modal"); 320 | /// modal.show(|ui| { 321 | /// modal.frame(ui, |ui| { 322 | /// modal.icon(ui, Icon::Info); 323 | /// }); 324 | /// }); 325 | /// ``` 326 | pub fn icon(&self, ui: &mut Ui, icon: Icon) { 327 | let color = match icon { 328 | Icon::Info => self.style.info_icon_color, 329 | Icon::Warning => self.style.warning_icon_color, 330 | Icon::Success => self.style.success_icon_color, 331 | Icon::Error => self.style.error_icon_color, 332 | Icon::Custom((_, color)) => color, 333 | }; 334 | let text = RichText::new(icon.to_string()) 335 | .color(color) 336 | .size(self.style.icon_size); 337 | ui_with_margin(ui, self.style.icon_margin, |ui| { 338 | ui.add(egui::Label::new(text)); 339 | }); 340 | } 341 | 342 | /// Helper function for styling the container the of body and icon. 343 | /// ``` 344 | /// let modal = Modal::new(ctx, "modal"); 345 | /// modal.show(|ui| { 346 | /// modal.title(ui, "my title"); 347 | /// modal.frame(ui, |ui| { 348 | /// // inner modal contents go here 349 | /// }); 350 | /// modal.buttons(ui, |ui| { 351 | /// // button contents go here 352 | /// }); 353 | /// }); 354 | /// ``` 355 | pub fn frame(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) { 356 | let last_frame_height = ModalState::load(&self.ctx, self.id) 357 | .last_frame_height 358 | .unwrap_or_default(); 359 | let default_height = self.style.default_height.unwrap_or_default(); 360 | let space_height = ((default_height - last_frame_height) * 0.5).max(0.); 361 | ui.with_layout( 362 | Layout::top_down(Align::Center).with_cross_align(Align::Center), 363 | |ui| { 364 | ui_with_margin(ui, self.style.frame_margin, |ui| { 365 | if space_height > 0. { 366 | ui.add_space(space_height); 367 | add_contents(ui); 368 | ui.add_space(space_height); 369 | } else { 370 | add_contents(ui); 371 | } 372 | }) 373 | }, 374 | ); 375 | } 376 | 377 | /// Helper function that should be used when using a body and icon together. 378 | /// ``` 379 | /// let modal = Modal::new(ctx, "modal"); 380 | /// modal.show(|ui| { 381 | /// modal.frame(ui, |ui| { 382 | /// modal.body_and_icon(ui, "my modal body", Icon::Warning); 383 | /// }); 384 | /// }); 385 | /// ``` 386 | pub fn body_and_icon(&self, ui: &mut Ui, text: impl Into, icon: Icon) { 387 | egui::Grid::new(self.id).num_columns(2).show(ui, |ui| { 388 | self.icon(ui, icon); 389 | self.body(ui, text); 390 | }); 391 | } 392 | 393 | /// Helper function for styling the body of the modal. 394 | /// ``` 395 | /// let modal = Modal::new(ctx, "modal"); 396 | /// modal.show(|ui| { 397 | /// modal.frame(ui, |ui| { 398 | /// modal.body(ui, "my modal body"); 399 | /// }); 400 | /// }); 401 | /// ``` 402 | pub fn body(&self, ui: &mut Ui, text: impl Into) { 403 | let text: WidgetText = text.into(); 404 | ui.with_layout(Layout::top_down(self.style.body_alignment), |ui| { 405 | ui_with_margin(ui, self.style.body_margin, |ui| { 406 | ui.label(text); 407 | }) 408 | }); 409 | } 410 | 411 | /// Helper function for styling the button container of the modal. 412 | /// ``` 413 | /// let modal = Modal::new(ctx, "modal"); 414 | /// modal.show(|ui| { 415 | /// modal.buttons(ui, |ui| { 416 | /// modal.button(ui, "my modal button"); 417 | /// }); 418 | /// }); 419 | /// ``` 420 | pub fn buttons(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) { 421 | ui.separator(); 422 | ui.with_layout(Layout::right_to_left(Align::Min), add_contents); 423 | } 424 | 425 | /// Helper function for creating a normal button for the modal. 426 | /// Automatically closes the modal on click. 427 | pub fn button(&self, ui: &mut Ui, text: impl Into) -> Response { 428 | self.styled_button(ui, text, ModalButtonStyle::None) 429 | } 430 | 431 | /// Helper function for creating a "cautioned" button for the modal. 432 | /// Automatically closes the modal on click. 433 | pub fn caution_button(&self, ui: &mut Ui, text: impl Into) -> Response { 434 | self.styled_button(ui, text, ModalButtonStyle::Caution) 435 | } 436 | 437 | /// Helper function for creating a "suggested" button for the modal. 438 | /// Automatically closes the modal on click. 439 | pub fn suggested_button(&self, ui: &mut Ui, text: impl Into) -> Response { 440 | self.styled_button(ui, text, ModalButtonStyle::Suggested) 441 | } 442 | 443 | fn styled_button( 444 | &self, 445 | ui: &mut Ui, 446 | text: impl Into, 447 | button_style: ModalButtonStyle, 448 | ) -> Response { 449 | let button = match button_style { 450 | ModalButtonStyle::Suggested => { 451 | let text: WidgetText = text.into().color(self.style.suggested_button_text_color); 452 | Button::new(text).fill(self.style.suggested_button_fill) 453 | } 454 | ModalButtonStyle::Caution => { 455 | let text: WidgetText = text.into().color(self.style.caution_button_text_color); 456 | Button::new(text).fill(self.style.caution_button_fill) 457 | } 458 | ModalButtonStyle::None => Button::new(text.into()), 459 | }; 460 | 461 | let response = ui.add(button); 462 | if response.clicked() { 463 | self.close() 464 | } 465 | response 466 | } 467 | 468 | /// The ui contained in this function will be shown within the modal window. The modal will only actually show 469 | /// when [`Modal::open`] is used. 470 | pub fn show(&self, add_contents: impl FnOnce(&mut Ui) -> R) { 471 | let mut modal_state = ModalState::load(&self.ctx, self.id); 472 | self.set_outside_clicked(false); 473 | if modal_state.is_open { 474 | let ctx_clone = self.ctx.clone(); 475 | let area_resp = Area::new(self.id) 476 | .interactable(true) 477 | .fixed_pos(Pos2::ZERO) 478 | .show(&self.ctx, |ui: &mut Ui| { 479 | let screen_rect = ui.ctx().input(|i| i.screen_rect); 480 | let area_response = ui.allocate_response(screen_rect.size(), Sense::click()); 481 | // let current_focus = area_response.ctx.memory().focus().clone(); 482 | // let top_layer = area_response.ctx.memory().layer_ids().last(); 483 | // if let Some(focus) = current_focus { 484 | // area_response.ctx.memory().surrender_focus(focus) 485 | // } 486 | if area_response.clicked() { 487 | self.set_outside_clicked(true); 488 | if self.close_on_outside_click { 489 | self.close(); 490 | } 491 | } 492 | ui.painter() 493 | .rect_filled(screen_rect, Rounding::ZERO, self.style.overlay_color); 494 | }); 495 | 496 | ctx_clone.move_to_top(area_resp.response.layer_id); 497 | 498 | // the below lines of code addresses a weird problem where if the default_height changes, egui doesnt respond unless 499 | // it's a different window id 500 | let mut window_id = self 501 | .style 502 | .default_width 503 | .map_or(self.window_id, |w| self.window_id.with(w.to_string())); 504 | 505 | window_id = self 506 | .style 507 | .default_height 508 | .map_or(window_id, |h| window_id.with(h.to_string())); 509 | 510 | let mut window = Window::new("") 511 | .id(window_id) 512 | .open(&mut modal_state.is_open) 513 | .title_bar(false) 514 | .anchor(Align2::CENTER_CENTER, [0., 0.]) 515 | .resizable(false); 516 | 517 | let recalculating_height = 518 | self.style.default_height.is_some() && modal_state.last_frame_height.is_none(); 519 | 520 | if let Some(default_height) = self.style.default_height { 521 | window = window.default_height(default_height); 522 | } 523 | 524 | if let Some(default_width) = self.style.default_width { 525 | window = window.default_width(default_width); 526 | } 527 | 528 | let response = window.show(&ctx_clone, add_contents); 529 | 530 | if let Some(inner_response) = response { 531 | ctx_clone.move_to_top(inner_response.response.layer_id); 532 | if recalculating_height { 533 | let mut modal_state = ModalState::load(&self.ctx, self.id); 534 | modal_state.last_frame_height = Some(inner_response.response.rect.height()); 535 | modal_state.save(&self.ctx, self.id); 536 | } 537 | } 538 | } 539 | } 540 | 541 | /// Open the modal as a dialog. This is a shorthand way of defining a [`Modal::show`] once, 542 | /// for example, if a function returns an `Error`. This should be used in conjunction with 543 | /// [`Modal::show_dialog`]. 544 | #[deprecated(since = "0.3.0", note = "use `Modal::dialog`")] 545 | pub fn open_dialog( 546 | &self, 547 | title: Option, 548 | body: Option, 549 | icon: Option, 550 | ) { 551 | let modal_data = DialogData { 552 | title: title.map(|s| s.to_string()), 553 | body: body.map(|s| s.to_string()), 554 | icon, 555 | }; 556 | let mut modal_state = ModalState::load(&self.ctx, self.id); 557 | modal_state.modal_type = ModalType::Dialog(modal_data); 558 | modal_state.is_open = true; 559 | modal_state.save(&self.ctx, self.id); 560 | } 561 | 562 | /// Create a `DialogBuilder` for this modal. Make sure to use `DialogBuilder::open` 563 | /// to open the dialog. 564 | pub fn dialog(&self) -> DialogBuilder { 565 | DialogBuilder { 566 | data: DialogData::default(), 567 | modal_id: self.id.clone(), 568 | ctx: self.ctx.clone(), 569 | } 570 | } 571 | 572 | /// Needed in order to use [`Modal::dialog`]. Make sure this is called every frame, as 573 | /// it renders the necessary ui when using a modal as a dialog. 574 | pub fn show_dialog(&mut self) { 575 | let modal_state = ModalState::load(&self.ctx, self.id); 576 | if let ModalType::Dialog(modal_data) = modal_state.modal_type { 577 | self.close_on_outside_click = true; 578 | self.show(|ui| { 579 | if let Some(title) = modal_data.title { 580 | self.title(ui, title) 581 | } 582 | self.frame(ui, |ui| { 583 | if modal_data.body.is_none() { 584 | if let Some(icon) = modal_data.icon { 585 | self.icon(ui, icon) 586 | } 587 | } else if modal_data.icon.is_none() { 588 | if let Some(body) = modal_data.body { 589 | self.body(ui, body) 590 | } 591 | } else if modal_data.icon.is_some() && modal_data.icon.is_some() { 592 | self.body_and_icon(ui, modal_data.body.unwrap(), modal_data.icon.unwrap()) 593 | } 594 | }); 595 | self.buttons(ui, |ui| { 596 | ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { 597 | self.button(ui, &self.style.dialog_ok_text) 598 | }) 599 | }) 600 | }); 601 | } 602 | } 603 | } 604 | 605 | impl DialogBuilder { 606 | /// Construct this dialog with the given title. 607 | pub fn with_title(mut self, title: impl std::fmt::Display) -> Self { 608 | self.data.title = Some(title.to_string()); 609 | self 610 | } 611 | /// Construct this dialog with the given body. 612 | pub fn with_body(mut self, body: impl std::fmt::Display) -> Self { 613 | self.data.body = Some(body.to_string()); 614 | self 615 | } 616 | /// Construct this dialog with the given icon. 617 | pub fn with_icon(mut self, icon: Icon) -> Self { 618 | self.data.icon = Some(icon); 619 | self 620 | } 621 | /// Open the dialog. 622 | /// 623 | /// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within 624 | /// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15) 625 | pub fn open(self) { 626 | let mut modal_state = ModalState::load(&self.ctx, self.modal_id); 627 | modal_state.modal_type = ModalType::Dialog(self.data); 628 | modal_state.is_open = true; 629 | modal_state.save(&self.ctx, self.modal_id); 630 | } 631 | } 632 | --------------------------------------------------------------------------------