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