configure_deck.rs (11505B)
1 use crate::{app_style::deck_icon_font_sized, deck_state::DeckState}; 2 use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget}; 3 use notedeck::{tr, Localization}; 4 use notedeck::{NamedFontFamily, NotedeckTextStyle}; 5 use notedeck_ui::{ 6 anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, 7 colors::PINK, 8 padding, 9 }; 10 11 pub struct ConfigureDeckView<'a> { 12 state: &'a mut DeckState, 13 create_button_text: String, 14 pub i18n: &'a mut Localization, 15 } 16 17 pub struct ConfigureDeckResponse { 18 pub icon: char, 19 pub name: String, 20 } 21 22 impl<'a> ConfigureDeckView<'a> { 23 pub fn new(state: &'a mut DeckState, i18n: &'a mut Localization) -> Self { 24 Self { 25 state, 26 create_button_text: tr!(i18n, "Create Deck", "Button label to create a new deck"), 27 i18n, 28 } 29 } 30 31 pub fn with_create_text(mut self, text: String) -> Self { 32 self.create_button_text = text; 33 self 34 } 35 36 pub fn scroll_id() -> egui::Id { 37 egui::Id::new("configure-deck") 38 } 39 40 pub fn ui(&mut self, ui: &mut Ui) -> Option<ConfigureDeckResponse> { 41 let title_font = egui::FontId::new( 42 notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4), 43 egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), 44 ); 45 padding(16.0, ui, |ui| { 46 ui.add(Label::new( 47 RichText::new(tr!( 48 self.i18n, 49 "Deck name", 50 "Label for deck name input field" 51 )) 52 .font(title_font.clone()), 53 )); 54 ui.add_space(8.0); 55 ui.text_edit_singleline(&mut self.state.deck_name); 56 ui.add_space(8.0); 57 ui.add(Label::new( 58 RichText::new(tr!( 59 self.i18n, 60 "We recommend short names", 61 "Hint for deck name input field" 62 )) 63 .color(ui.visuals().noninteractive().fg_stroke.color) 64 .size(notedeck::fonts::get_font_size( 65 ui.ctx(), 66 &NotedeckTextStyle::Small, 67 )), 68 )); 69 70 ui.add_space(32.0); 71 ui.add(Label::new( 72 RichText::new(tr!(self.i18n, "Icon", "Label for deck icon selection")) 73 .font(title_font), 74 )); 75 76 if ui 77 .add(deck_icon( 78 ui.id().with("config-deck"), 79 self.state.selected_glyph, 80 38.0, 81 64.0, 82 false, 83 )) 84 .clicked() 85 { 86 self.state.selecting_glyph = !self.state.selecting_glyph; 87 } 88 89 if self.state.selecting_glyph { 90 let max_height = if ui.available_height() - 100.0 > 0.0 { 91 ui.available_height() - 100.0 92 } else { 93 ui.available_height() 94 }; 95 egui::Frame::window(ui.style()).show(ui, |ui| { 96 let glyphs = self.state.get_glyph_options(ui); 97 if let Some(selected_glyph) = glyph_options_ui(ui, 16.0, max_height, glyphs) { 98 self.state.selected_glyph = Some(selected_glyph); 99 self.state.selecting_glyph = false; 100 } 101 }); 102 ui.add_space(16.0); 103 } 104 105 if self.state.warn_no_icon && self.state.selected_glyph.is_some() { 106 self.state.warn_no_icon = false; 107 } 108 if self.state.warn_no_title && !self.state.deck_name.is_empty() { 109 self.state.warn_no_title = false; 110 } 111 112 show_warnings( 113 ui, 114 self.i18n, 115 self.state.warn_no_icon, 116 self.state.warn_no_title, 117 ); 118 119 let mut resp = None; 120 if ui 121 .add(create_deck_button(&self.create_button_text)) 122 .clicked() 123 { 124 if self.state.deck_name.is_empty() { 125 self.state.warn_no_title = true; 126 } 127 if self.state.selected_glyph.is_none() { 128 self.state.warn_no_icon = true; 129 } 130 if !self.state.deck_name.is_empty() { 131 if let Some(glyph) = self.state.selected_glyph { 132 resp = Some(ConfigureDeckResponse { 133 icon: glyph, 134 name: self.state.deck_name.clone(), 135 }); 136 } 137 } 138 } 139 resp 140 }) 141 .inner 142 } 143 } 144 145 fn show_warnings(ui: &mut Ui, i18n: &mut Localization, warn_no_icon: bool, warn_no_title: bool) { 146 let warning = if warn_no_title && warn_no_icon { 147 tr!( 148 i18n, 149 "Please create a name for the deck and select an icon.", 150 "Error message for missing deck name and icon" 151 ) 152 } else if warn_no_title { 153 tr!( 154 i18n, 155 "Please create a name for the deck.", 156 "Error message for missing deck name" 157 ) 158 } else if warn_no_icon { 159 tr!( 160 i18n, 161 "Please select an icon.", 162 "Error message for missing deck icon" 163 ) 164 } else { 165 String::new() 166 }; 167 168 if !warning.is_empty() { 169 ui.add(egui::Label::new(RichText::new(warning).color(ui.visuals().error_fg_color)).wrap()); 170 } 171 } 172 173 fn create_deck_button(text: &str) -> impl Widget + '_ { 174 move |ui: &mut egui::Ui| { 175 let size = vec2(108.0, 40.0); 176 ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| { 177 ui.add(Button::new(text).fill(PINK).min_size(size)) 178 }) 179 .inner 180 } 181 } 182 183 pub fn deck_icon( 184 id: egui::Id, 185 glyph: Option<char>, 186 font_size: f32, 187 full_size: f32, 188 highlight: bool, 189 ) -> impl Widget { 190 move |ui: &mut egui::Ui| -> egui::Response { 191 let max_size = full_size * ICON_EXPANSION_MULTIPLE; 192 193 let helper = AnimationHelper::new(ui, id, vec2(max_size, max_size)); 194 let painter = ui.painter_at(helper.get_animation_rect()); 195 let bg_center = helper.get_animation_rect().center(); 196 197 let (stroke, fill_color) = if highlight { 198 ( 199 ui.visuals().selection.stroke, 200 ui.visuals().widgets.noninteractive.weak_bg_fill, 201 ) 202 } else { 203 ( 204 Stroke::new( 205 ui.visuals().widgets.inactive.bg_stroke.width, 206 ui.visuals().widgets.inactive.weak_bg_fill, 207 ), 208 ui.visuals().widgets.noninteractive.weak_bg_fill, 209 ) 210 }; 211 212 let radius = helper.scale_1d_pos((full_size / 2.0) - stroke.width); 213 painter.circle(bg_center, radius, fill_color, stroke); 214 215 if let Some(glyph) = glyph { 216 let font = 217 deck_icon_font_sized(helper.scale_1d_pos(font_size / std::f32::consts::SQRT_2)); 218 let glyph_galley = 219 painter.layout_no_wrap(glyph.to_string(), font, ui.visuals().text_color()); 220 221 let top_left = { 222 let mut glyph_rect = glyph_galley.rect; 223 glyph_rect.set_center(bg_center); 224 glyph_rect.left_top() 225 }; 226 227 painter.galley(top_left, glyph_galley, Color32::WHITE); 228 } 229 230 helper.take_animation_response() 231 } 232 } 233 234 fn glyph_icon_max_size(ui: &egui::Ui, glyph: &char, font_size: f32) -> egui::Vec2 { 235 let painter = ui.painter(); 236 let font = deck_icon_font_sized(font_size * ICON_EXPANSION_MULTIPLE); 237 let glyph_galley = painter.layout_no_wrap(glyph.to_string(), font, Color32::WHITE); 238 glyph_galley.rect.size() 239 } 240 241 fn glyph_icon(glyph: char, font_size: f32, max_size: egui::Vec2, color: Color32) -> impl Widget { 242 move |ui: &mut egui::Ui| { 243 let helper = AnimationHelper::new(ui, ("glyph", glyph), max_size); 244 let painter = ui.painter_at(helper.get_animation_rect()); 245 246 let font = deck_icon_font_sized(helper.scale_1d_pos(font_size)); 247 let glyph_galley = painter.layout_no_wrap(glyph.to_string(), font, color); 248 249 let top_left = { 250 let mut glyph_rect = glyph_galley.rect; 251 glyph_rect.set_center(helper.get_animation_rect().center()); 252 glyph_rect.left_top() 253 }; 254 255 painter.galley(top_left, glyph_galley, Color32::WHITE); 256 helper.take_animation_response() 257 } 258 } 259 260 fn glyph_options_ui( 261 ui: &mut egui::Ui, 262 font_size: f32, 263 max_height: f32, 264 glyphs: &[char], 265 ) -> Option<char> { 266 let mut selected_glyph = None; 267 egui::ScrollArea::vertical() 268 .id_salt(ConfigureDeckView::scroll_id()) 269 .max_height(max_height) 270 .show(ui, |ui| { 271 let max_width = ui.available_width(); 272 let mut row_glyphs = Vec::new(); 273 let mut cur_width = 0.0; 274 let spacing = ui.spacing().item_spacing.x; 275 276 for (index, glyph) in glyphs.iter().enumerate() { 277 let next_glyph_size = glyph_icon_max_size(ui, glyph, font_size); 278 279 if cur_width + spacing + next_glyph_size.x > max_width { 280 if let Some(selected) = paint_row(ui, &row_glyphs, font_size) { 281 selected_glyph = Some(selected); 282 } 283 row_glyphs.clear(); 284 cur_width = 0.0; 285 } 286 287 cur_width += spacing; 288 cur_width += next_glyph_size.x; 289 row_glyphs.push(*glyph); 290 291 if index == glyphs.len() - 1 { 292 if let Some(selected) = paint_row(ui, &row_glyphs, font_size) { 293 selected_glyph = Some(selected); 294 } 295 } 296 } 297 }); 298 selected_glyph 299 } 300 301 fn paint_row(ui: &mut egui::Ui, row_glyphs: &[char], font_size: f32) -> Option<char> { 302 let mut selected_glyph = None; 303 ui.horizontal(|ui| { 304 for glyph in row_glyphs { 305 let glyph_size = glyph_icon_max_size(ui, glyph, font_size); 306 if ui 307 .add(glyph_icon( 308 *glyph, 309 font_size, 310 glyph_size, 311 ui.visuals().text_color(), 312 )) 313 .clicked() 314 { 315 selected_glyph = Some(*glyph); 316 } 317 } 318 }); 319 selected_glyph 320 } 321 322 mod preview { 323 use crate::{ 324 deck_state::DeckState, 325 ui::{Preview, PreviewConfig}, 326 }; 327 328 use super::ConfigureDeckView; 329 use notedeck::{App, AppAction, AppContext}; 330 331 pub struct ConfigureDeckPreview { 332 state: DeckState, 333 } 334 335 impl ConfigureDeckPreview { 336 fn new() -> Self { 337 let state = DeckState::default(); 338 339 ConfigureDeckPreview { state } 340 } 341 } 342 343 impl App for ConfigureDeckPreview { 344 fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { 345 ConfigureDeckView::new(&mut self.state, ctx.i18n).ui(ui); 346 347 None 348 } 349 } 350 351 impl Preview for ConfigureDeckView<'_> { 352 type Prev = ConfigureDeckPreview; 353 354 fn preview(_cfg: PreviewConfig) -> Self::Prev { 355 ConfigureDeckPreview::new() 356 } 357 } 358 }