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