@@ -0,0 +1,629 @@
use egui ::{
emath ::{ Align , Align2 } , epaint ::{ Color32 , Pos2 } , Area , Button , Context , CornerRadius , Id , Layout , Response , RichText , Sense , Ui , WidgetText , Window
} ;
const ERROR_ICON_COLOR : Color32 = Color32 ::from_rgb ( 200 , 90 , 90 ) ;
const INFO_ICON_COLOR : Color32 = Color32 ::from_rgb ( 150 , 200 , 210 ) ;
const WARNING_ICON_COLOR : Color32 = Color32 ::from_rgb ( 230 , 220 , 140 ) ;
const SUCCESS_ICON_COLOR : Color32 = Color32 ::from_rgb ( 140 , 230 , 140 ) ;
const CAUTION_BUTTON_FILL : Color32 = Color32 ::from_rgb ( 87 , 38 , 34 ) ;
const SUGGESTED_BUTTON_FILL : Color32 = Color32 ::from_rgb ( 33 , 54 , 84 ) ;
const CAUTION_BUTTON_TEXT_COLOR : Color32 = Color32 ::from_rgb ( 242 , 148 , 148 ) ;
const SUGGESTED_BUTTON_TEXT_COLOR : Color32 = Color32 ::from_rgb ( 141 , 182 , 242 ) ;
const OVERLAY_COLOR : Color32 = Color32 ::from_rgba_premultiplied ( 0 , 0 , 0 , 200 ) ;
/// The different styles a modal button can take.
pub enum ModalButtonStyle {
/// A normal [`egui`] button
None ,
/// A button highlighted blue
Suggested ,
/// A button highlighted red
Caution ,
}
/// An icon. If used, it will be shown next to the body of
/// the modal.
#[ derive(Clone, Default, PartialEq) ]
pub enum Icon {
#[ default ]
/// An info icon
Info ,
/// A warning icon
Warning ,
/// A success icon
Success ,
/// An error icon
Error ,
/// A custom icon. The first field in the tuple is
/// the text of the icon, and the second field is the
/// color.
Custom ( ( String , Color32 ) ) ,
}
impl std ::fmt ::Display for Icon {
fn fmt ( & self , f : & mut std ::fmt ::Formatter ) -> std ::fmt ::Result {
match self {
Icon ::Info = > write! ( f , " ℹ " ) ,
Icon ::Warning = > write! ( f , " ⚠ " ) ,
Icon ::Success = > write! ( f , " ✔ " ) ,
Icon ::Error = > write! ( f , " ❗ " ) ,
Icon ::Custom ( ( icon_text , _ ) ) = > write! ( f , " {icon_text} " ) ,
}
}
}
#[ derive(Clone, Default) ]
struct DialogData {
title : Option < String > ,
body : Option < String > ,
icon : Option < Icon > ,
}
/// Used for constructing and opening a modal dialog. This can be used
/// to both set the title/body/icon of the modal and open it as a one-time call
/// (as opposed to a continous call in the update loop) at the same time.
/// Make sure to call `DialogBuilder::open` to actually open the dialog.
#[ must_use = " use `DialogBuilder::open` " ]
pub struct DialogBuilder {
data : DialogData ,
modal_id : Id ,
ctx : Context ,
}
#[ derive(Clone) ]
enum ModalType {
Modal ,
Dialog ( DialogData ) ,
}
#[ derive(Clone) ]
/// Information about the current state of the modal. (Pretty empty
/// right now but may be expanded upon in the future.)
struct ModalState {
is_open : bool ,
was_outside_clicked : bool ,
modal_type : ModalType ,
last_frame_height : Option < f32 > ,
}
#[ derive(Clone, Debug) ]
/// Contains styling parameters for the modal, like body margin
/// and button colors.
pub struct ModalStyle {
/// The margin around the modal body. Only applies if using
/// [`.body()`]
pub body_margin : f32 ,
/// The margin around the container of the icon and body. Only
/// applies if using [`.frame()`]
pub frame_margin : f32 ,
/// The margin around the container of the icon. Only applies
/// if using [`.icon()`].
pub icon_margin : f32 ,
/// The size of any icons used in the modal
pub icon_size : f32 ,
/// The color of the overlay that dims the background
pub overlay_color : Color32 ,
/// The fill color for the caution button style
pub caution_button_fill : Color32 ,
/// The fill color for the suggested button style
pub suggested_button_fill : Color32 ,
/// The text color for the caution button style
pub caution_button_text_color : Color32 ,
/// The text color for the suggested button style
pub suggested_button_text_color : Color32 ,
/// The text of the acknowledgement button for dialogs
pub dialog_ok_text : String ,
/// The color of the info icon
pub info_icon_color : Color32 ,
/// The color of the warning icon
pub warning_icon_color : Color32 ,
/// The color of the success icon
pub success_icon_color : Color32 ,
/// The color of the error icon
pub error_icon_color : Color32 ,
/// The default width of the modal
pub default_width : Option < f32 > ,
/// The default height of the modal
pub default_height : Option < f32 > ,
/// The alignment of text inside the body
pub body_alignment : Align ,
}
impl ModalState {
fn load ( ctx : & Context , id : Id ) -> Self {
ctx . data_mut ( | d | d . get_temp ( id ) . unwrap_or_default ( ) )
}
fn save ( self , ctx : & Context , id : Id ) {
ctx . data_mut ( | d | d . insert_temp ( id , self ) )
}
}
impl Default for ModalState {
fn default ( ) -> Self {
Self {
was_outside_clicked : false ,
is_open : false ,
modal_type : ModalType ::Modal ,
last_frame_height : None ,
}
}
}
impl Default for ModalStyle {
fn default ( ) -> Self {
Self {
body_margin : 5. ,
icon_margin : 7. ,
frame_margin : 2. ,
icon_size : 30. ,
overlay_color : OVERLAY_COLOR ,
caution_button_fill : CAUTION_BUTTON_FILL ,
suggested_button_fill : SUGGESTED_BUTTON_FILL ,
caution_button_text_color : CAUTION_BUTTON_TEXT_COLOR ,
suggested_button_text_color : SUGGESTED_BUTTON_TEXT_COLOR ,
dialog_ok_text : " ok " . to_string ( ) ,
info_icon_color : INFO_ICON_COLOR ,
warning_icon_color : WARNING_ICON_COLOR ,
success_icon_color : SUCCESS_ICON_COLOR ,
error_icon_color : ERROR_ICON_COLOR ,
default_height : None ,
default_width : None ,
body_alignment : Align ::Min ,
}
}
}
/// A [`Modal`] is created using [`Modal::new()`]. Make sure to use a `let` binding when
/// using [`Modal::new()`] to ensure you can call things like [`Modal::open()`] later on.
/// ```
/// let modal = Modal::new(ctx, "my_modal");
/// modal.show(|ui| {
/// ui.label("Hello world!")
/// });
/// if ui.button("modal").clicked() {
/// modal.open();
/// }
/// ```
/// Helper functions are also available to use that help apply margins based on the modal's
/// [`ModalStyle`]. They are not necessary to use, but may help reduce boilerplate.
/// ```
/// let other_modal = Modal::new(ctx, "another_modal");
/// other_modal.show(|ui| {
/// other_modal.frame(ui, |ui| {
/// other_modal.body(ui, "Hello again, world!");
/// });
/// other_modal.buttons(ui, |ui| {
/// other_modal.button(ui, "Close");
/// });
/// });
/// if ui.button("open the other modal").clicked() {
/// other_modal.open();
/// }
/// ```
pub struct Modal {
close_on_outside_click : bool ,
style : ModalStyle ,
ctx : Context ,
id : Id ,
window_id : Id ,
}
fn ui_with_margin < R > ( ui : & mut Ui , margin : f32 , add_contents : impl FnOnce ( & mut Ui ) -> R ) {
egui ::Frame ::NONE
. inner_margin ( margin )
. show ( ui , | ui | add_contents ( ui ) ) ;
}
impl Modal {
/// Creates a new [`Modal`]. Can use constructor functions like [`Modal::with_style`]
/// to modify upon creation.
pub fn new ( ctx : & Context , id_source : impl std ::fmt ::Display ) -> Self {
let self_id = Id ::new ( id_source . to_string ( ) ) ;
Self {
window_id : self_id . with ( " window " ) ,
id : self_id ,
style : ModalStyle ::default ( ) ,
ctx : ctx . clone ( ) ,
close_on_outside_click : false ,
}
}
fn set_open_state ( & self , is_open : bool ) {
let mut modal_state = ModalState ::load ( & self . ctx , self . id ) ;
modal_state . is_open = is_open ;
modal_state . save ( & self . ctx , self . id )
}
fn set_outside_clicked ( & self , was_clicked : bool ) {
let mut modal_state = ModalState ::load ( & self . ctx , self . id ) ;
modal_state . was_outside_clicked = was_clicked ;
modal_state . save ( & self . ctx , self . id )
}
/// Was the outer overlay clicked this frame?
pub fn was_outside_clicked ( & self ) -> bool {
let modal_state = ModalState ::load ( & self . ctx , self . id ) ;
modal_state . was_outside_clicked
}
/// Is the modal currently open?
pub fn is_open ( & self ) -> bool {
let modal_state = ModalState ::load ( & self . ctx , self . id ) ;
modal_state . is_open
}
/// Open the modal; make it visible. The modal prevents user input to other parts of the
/// application.
///
/// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within
/// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15)
pub fn open ( & self ) {
self . set_open_state ( true )
}
/// Close the modal so that it is no longer visible, allowing input to flow back into
/// the application.
///
/// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within
/// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15)
pub fn close ( & self ) {
self . set_open_state ( false )
}
/// If set to `true`, the modal will close itself if the user clicks outside on the modal window
/// (onto the overlay).
pub fn with_close_on_outside_click ( mut self , do_close_on_click_ouside : bool ) -> Self {
self . close_on_outside_click = do_close_on_click_ouside ;
self
}
/// Change the [`ModalStyle`] of the modal upon creation.
pub fn with_style ( mut self , style : & ModalStyle ) -> Self {
self . style = style . clone ( ) ;
self
}
/// Helper function for styling the title of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.title(ui, "my title");
/// });
/// ```
pub fn title ( & self , ui : & mut Ui , text : impl Into < RichText > ) {
let text : RichText = text . into ( ) ;
ui . vertical_centered ( | ui | {
ui . heading ( text ) ;
} ) ;
ui . separator ( ) ;
}
/// Helper function for styling the icon of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.frame(ui, |ui| {
/// modal.icon(ui, Icon::Info);
/// });
/// });
/// ```
pub fn icon ( & self , ui : & mut Ui , icon : Icon ) {
let color = match icon {
Icon ::Info = > self . style . info_icon_color ,
Icon ::Warning = > self . style . warning_icon_color ,
Icon ::Success = > self . style . success_icon_color ,
Icon ::Error = > self . style . error_icon_color ,
Icon ::Custom ( ( _ , color ) ) = > color ,
} ;
let text = RichText ::new ( icon . to_string ( ) )
. color ( color )
. size ( self . style . icon_size ) ;
ui_with_margin ( ui , self . style . icon_margin , | ui | {
ui . add ( egui ::Label ::new ( text ) ) ;
} ) ;
}
/// Helper function for styling the container the of body and icon.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.title(ui, "my title");
/// modal.frame(ui, |ui| {
/// // inner modal contents go here
/// });
/// modal.buttons(ui, |ui| {
/// // button contents go here
/// });
/// });
/// ```
pub fn frame < R > ( & self , ui : & mut Ui , add_contents : impl FnOnce ( & mut Ui ) -> R ) {
let last_frame_height = ModalState ::load ( & self . ctx , self . id )
. last_frame_height
. unwrap_or_default ( ) ;
let default_height = self . style . default_height . unwrap_or_default ( ) ;
let space_height = ( ( default_height - last_frame_height ) * 0.5 ) . max ( 0. ) ;
ui . with_layout (
Layout ::top_down ( Align ::Center ) . with_cross_align ( Align ::Center ) ,
| ui | {
ui_with_margin ( ui , self . style . frame_margin , | ui | {
if space_height > 0. {
ui . add_space ( space_height ) ;
add_contents ( ui ) ;
ui . add_space ( space_height ) ;
} else {
add_contents ( ui ) ;
}
} )
} ,
) ;
}
/// Helper function that should be used when using a body and icon together.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.frame(ui, |ui| {
/// modal.body_and_icon(ui, "my modal body", Icon::Warning);
/// });
/// });
/// ```
pub fn body_and_icon ( & self , ui : & mut Ui , text : impl Into < WidgetText > , icon : Icon ) {
egui ::Grid ::new ( self . id ) . num_columns ( 2 ) . show ( ui , | ui | {
self . icon ( ui , icon ) ;
self . body ( ui , text ) ;
} ) ;
}
/// Helper function for styling the body of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.frame(ui, |ui| {
/// modal.body(ui, "my modal body");
/// });
/// });
/// ```
pub fn body ( & self , ui : & mut Ui , text : impl Into < WidgetText > ) {
let text : WidgetText = text . into ( ) ;
ui . with_layout ( Layout ::top_down ( self . style . body_alignment ) , | ui | {
ui_with_margin ( ui , self . style . body_margin , | ui | {
ui . label ( text ) ;
} )
} ) ;
}
/// Helper function for styling the button container of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.buttons(ui, |ui| {
/// modal.button(ui, "my modal button");
/// });
/// });
/// ```
pub fn buttons < R > ( & self , ui : & mut Ui , add_contents : impl FnOnce ( & mut Ui ) -> R ) {
ui . separator ( ) ;
ui . with_layout ( Layout ::right_to_left ( Align ::Min ) , add_contents ) ;
}
/// Helper function for creating a normal button for the modal.
/// Automatically closes the modal on click.
pub fn button ( & self , ui : & mut Ui , text : impl Into < WidgetText > ) -> Response {
self . styled_button ( ui , text , ModalButtonStyle ::None )
}
/// Helper function for creating a "cautioned" button for the modal.
/// Automatically closes the modal on click.
pub fn caution_button ( & self , ui : & mut Ui , text : impl Into < WidgetText > ) -> Response {
self . styled_button ( ui , text , ModalButtonStyle ::Caution )
}
/// Helper function for creating a "suggested" button for the modal.
/// Automatically closes the modal on click.
pub fn suggested_button ( & self , ui : & mut Ui , text : impl Into < WidgetText > ) -> Response {
self . styled_button ( ui , text , ModalButtonStyle ::Suggested )
}
fn styled_button (
& self ,
ui : & mut Ui ,
text : impl Into < WidgetText > ,
button_style : ModalButtonStyle ,
) -> Response {
let button = match button_style {
ModalButtonStyle ::Suggested = > {
let text : WidgetText = text . into ( ) . color ( self . style . suggested_button_text_color ) ;
Button ::new ( text ) . fill ( self . style . suggested_button_fill )
}
ModalButtonStyle ::Caution = > {
let text : WidgetText = text . into ( ) . color ( self . style . caution_button_text_color ) ;
Button ::new ( text ) . fill ( self . style . caution_button_fill )
}
ModalButtonStyle ::None = > Button ::new ( text . into ( ) ) ,
} ;
let response = ui . add ( button ) ;
if response . clicked ( ) {
self . close ( )
}
response
}
/// The ui contained in this function will be shown within the modal window. The modal will only actually show
/// when [`Modal::open`] is used.
pub fn show < R > ( & self , add_contents : impl FnOnce ( & mut Ui ) -> R ) {
let mut modal_state = ModalState ::load ( & self . ctx , self . id ) ;
self . set_outside_clicked ( false ) ;
if modal_state . is_open {
let ctx_clone = self . ctx . clone ( ) ;
let area_resp = Area ::new ( self . id )
. interactable ( true )
. fixed_pos ( Pos2 ::ZERO )
. show ( & self . ctx , | ui : & mut Ui | {
let screen_rect = ui . ctx ( ) . input ( | i | i . screen_rect ) ;
let area_response = ui . allocate_response ( screen_rect . size ( ) , Sense ::click ( ) ) ;
// let current_focus = area_response.ctx.memory().focus().clone();
// let top_layer = area_response.ctx.memory().layer_ids().last();
// if let Some(focus) = current_focus {
// area_response.ctx.memory().surrender_focus(focus)
// }
if area_response . clicked ( ) {
self . set_outside_clicked ( true ) ;
if self . close_on_outside_click {
self . close ( ) ;
}
}
ui . painter ( )
. rect_filled ( screen_rect , CornerRadius ::ZERO , self . style . overlay_color ) ;
} ) ;
ctx_clone . move_to_top ( area_resp . response . layer_id ) ;
// the below lines of code addresses a weird problem where if the default_height changes, egui doesnt respond unless
// it's a different window id
let mut window_id = self
. style
. default_width
. map_or ( self . window_id , | w | self . window_id . with ( w . to_string ( ) ) ) ;
window_id = self
. style
. default_height
. map_or ( window_id , | h | window_id . with ( h . to_string ( ) ) ) ;
let mut window = Window ::new ( " " )
. id ( window_id )
. open ( & mut modal_state . is_open )
. title_bar ( false )
. anchor ( Align2 ::CENTER_CENTER , [ 0. , 0. ] )
. resizable ( false ) ;
let recalculating_height =
self . style . default_height . is_some ( ) & & modal_state . last_frame_height . is_none ( ) ;
if let Some ( default_height ) = self . style . default_height {
window = window . default_height ( default_height ) ;
}
if let Some ( default_width ) = self . style . default_width {
window = window . default_width ( default_width ) ;
}
let response = window . show ( & ctx_clone , add_contents ) ;
if let Some ( inner_response ) = response {
ctx_clone . move_to_top ( inner_response . response . layer_id ) ;
if recalculating_height {
let mut modal_state = ModalState ::load ( & self . ctx , self . id ) ;
modal_state . last_frame_height = Some ( inner_response . response . rect . height ( ) ) ;
modal_state . save ( & self . ctx , self . id ) ;
}
}
}
}
/// Open the modal as a dialog. This is a shorthand way of defining a [`Modal::show`] once,
/// for example, if a function returns an `Error`. This should be used in conjunction with
/// [`Modal::show_dialog`].
#[ deprecated(since = " 0.3.0 " , note = " use `Modal::dialog` " ) ]
pub fn open_dialog (
& self ,
title : Option < impl std ::fmt ::Display > ,
body : Option < impl std ::fmt ::Display > ,
icon : Option < Icon > ,
) {
let modal_data = DialogData {
title : title . map ( | s | s . to_string ( ) ) ,
body : body . map ( | s | s . to_string ( ) ) ,
icon ,
} ;
let mut modal_state = ModalState ::load ( & self . ctx , self . id ) ;
modal_state . modal_type = ModalType ::Dialog ( modal_data ) ;
modal_state . is_open = true ;
modal_state . save ( & self . ctx , self . id ) ;
}
/// Create a `DialogBuilder` for this modal. Make sure to use `DialogBuilder::open`
/// to open the dialog.
pub fn dialog ( & self ) -> DialogBuilder {
DialogBuilder {
data : DialogData ::default ( ) ,
modal_id : self . id . clone ( ) ,
ctx : self . ctx . clone ( ) ,
}
}
/// Needed in order to use [`Modal::dialog`]. Make sure this is called every frame, as
/// it renders the necessary ui when using a modal as a dialog.
pub fn show_dialog ( & mut self ) {
let modal_state = ModalState ::load ( & self . ctx , self . id ) ;
if let ModalType ::Dialog ( modal_data ) = modal_state . modal_type {
self . close_on_outside_click = true ;
self . show ( | ui | {
if let Some ( title ) = modal_data . title {
self . title ( ui , title )
}
self . frame ( ui , | ui | {
if modal_data . body . is_none ( ) {
if let Some ( icon ) = modal_data . icon {
self . icon ( ui , icon )
}
} else if modal_data . icon . is_none ( ) {
if let Some ( body ) = modal_data . body {
self . body ( ui , body )
}
} else if modal_data . icon . is_some ( ) & & modal_data . icon . is_some ( ) {
self . body_and_icon ( ui , modal_data . body . unwrap ( ) , modal_data . icon . unwrap ( ) )
}
} ) ;
self . buttons ( ui , | ui | {
ui . with_layout ( Layout ::top_down_justified ( Align ::Center ) , | ui | {
self . button ( ui , & self . style . dialog_ok_text )
} )
} )
} ) ;
}
}
}
impl DialogBuilder {
/// Construct this dialog with the given title.
pub fn with_title ( mut self , title : impl std ::fmt ::Display ) -> Self {
self . data . title = Some ( title . to_string ( ) ) ;
self
}
/// Construct this dialog with the given body.
pub fn with_body ( mut self , body : impl std ::fmt ::Display ) -> Self {
self . data . body = Some ( body . to_string ( ) ) ;
self
}
/// Construct this dialog with the given icon.
pub fn with_icon ( mut self , icon : Icon ) -> Self {
self . data . icon = Some ( icon ) ;
self
}
/// Open the dialog.
///
/// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within
/// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15)
pub fn open ( self ) {
let mut modal_state = ModalState ::load ( & self . ctx , self . modal_id ) ;
modal_state . modal_type = ModalType ::Dialog ( self . data ) ;
modal_state . is_open = true ;
modal_state . save ( & self . ctx , self . modal_id ) ;
}
}