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