use crate::{app_style::deck_icon_font_sized, deck_state::DeckState}; use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget}; use notedeck::{NamedFontFamily, NotedeckTextStyle}; use notedeck_ui::colors::PINK; use super::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, padding, }; pub struct ConfigureDeckView<'a> { state: &'a mut DeckState, create_button_text: String, } pub struct ConfigureDeckResponse { pub icon: char, pub name: String, } static CREATE_TEXT: &str = "Create Deck"; impl<'a> ConfigureDeckView<'a> { pub fn new(state: &'a mut DeckState) -> Self { Self { state, create_button_text: CREATE_TEXT.to_owned(), } } pub fn with_create_text(mut self, text: &str) -> Self { self.create_button_text = text.to_owned(); self } pub fn ui(&mut self, ui: &mut Ui) -> Option { let title_font = egui::FontId::new( notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4), egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), ); padding(16.0, ui, |ui| { ui.add(Label::new( RichText::new("Deck name").font(title_font.clone()), )); ui.add_space(8.0); ui.text_edit_singleline(&mut self.state.deck_name); ui.add_space(8.0); ui.add(Label::new( RichText::new("We recommend short names") .color(ui.visuals().noninteractive().fg_stroke.color) .size(notedeck::fonts::get_font_size( ui.ctx(), &NotedeckTextStyle::Small, )), )); ui.add_space(32.0); ui.add(Label::new(RichText::new("Icon").font(title_font))); if ui .add(deck_icon( ui.id().with("config-deck"), self.state.selected_glyph, 38.0, 64.0, false, )) .clicked() { self.state.selecting_glyph = !self.state.selecting_glyph; } if self.state.selecting_glyph { let max_height = if ui.available_height() - 100.0 > 0.0 { ui.available_height() - 100.0 } else { ui.available_height() }; egui::Frame::window(ui.style()).show(ui, |ui| { let glyphs = self.state.get_glyph_options(ui); if let Some(selected_glyph) = glyph_options_ui(ui, 16.0, max_height, glyphs) { self.state.selected_glyph = Some(selected_glyph); self.state.selecting_glyph = false; } }); ui.add_space(16.0); } if self.state.warn_no_icon && self.state.selected_glyph.is_some() { self.state.warn_no_icon = false; } if self.state.warn_no_title && !self.state.deck_name.is_empty() { self.state.warn_no_title = false; } show_warnings(ui, self.state.warn_no_icon, self.state.warn_no_title); let mut resp = None; if ui .add(create_deck_button(&self.create_button_text)) .clicked() { if self.state.deck_name.is_empty() { self.state.warn_no_title = true; } if self.state.selected_glyph.is_none() { self.state.warn_no_icon = true; } if !self.state.deck_name.is_empty() { if let Some(glyph) = self.state.selected_glyph { resp = Some(ConfigureDeckResponse { icon: glyph, name: self.state.deck_name.clone(), }); } } } resp }) .inner } } fn show_warnings(ui: &mut Ui, warn_no_icon: bool, warn_no_title: bool) { if warn_no_icon || warn_no_title { let messages = [ if warn_no_title { "create a name for the deck" } else { "" }, if warn_no_icon { "select an icon" } else { "" }, ]; let message = messages .iter() .filter(|&&m| !m.is_empty()) .copied() .collect::>() .join(" and "); ui.add( egui::Label::new( RichText::new(format!("Please {}.", message)).color(ui.visuals().error_fg_color), ) .wrap(), ); } } fn create_deck_button(text: &str) -> impl Widget + '_ { move |ui: &mut egui::Ui| { let size = vec2(108.0, 40.0); ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| { ui.add(Button::new(text).fill(PINK).min_size(size)) }) .inner } } pub fn deck_icon( id: egui::Id, glyph: Option, font_size: f32, full_size: f32, highlight: bool, ) -> impl Widget { move |ui: &mut egui::Ui| -> egui::Response { let max_size = full_size * ICON_EXPANSION_MULTIPLE; let helper = AnimationHelper::new(ui, id, vec2(max_size, max_size)); let painter = ui.painter_at(helper.get_animation_rect()); let bg_center = helper.get_animation_rect().center(); let (stroke, fill_color) = if highlight { ( ui.visuals().selection.stroke, ui.visuals().widgets.noninteractive.weak_bg_fill, ) } else { ( Stroke::new( ui.visuals().widgets.inactive.bg_stroke.width, ui.visuals().widgets.inactive.weak_bg_fill, ), ui.visuals().widgets.noninteractive.weak_bg_fill, ) }; let radius = helper.scale_1d_pos((full_size / 2.0) - stroke.width); painter.circle(bg_center, radius, fill_color, stroke); if let Some(glyph) = glyph { let font = deck_icon_font_sized(helper.scale_1d_pos(font_size / std::f32::consts::SQRT_2)); let glyph_galley = painter.layout_no_wrap(glyph.to_string(), font, ui.visuals().text_color()); let top_left = { let mut glyph_rect = glyph_galley.rect; glyph_rect.set_center(bg_center); glyph_rect.left_top() }; painter.galley(top_left, glyph_galley, Color32::WHITE); } helper.take_animation_response() } } fn glyph_icon_max_size(ui: &egui::Ui, glyph: &char, font_size: f32) -> egui::Vec2 { let painter = ui.painter(); let font = deck_icon_font_sized(font_size * ICON_EXPANSION_MULTIPLE); let glyph_galley = painter.layout_no_wrap(glyph.to_string(), font, Color32::WHITE); glyph_galley.rect.size() } fn glyph_icon(glyph: char, font_size: f32, max_size: egui::Vec2, color: Color32) -> impl Widget { move |ui: &mut egui::Ui| { let helper = AnimationHelper::new(ui, ("glyph", glyph), max_size); let painter = ui.painter_at(helper.get_animation_rect()); let font = deck_icon_font_sized(helper.scale_1d_pos(font_size)); let glyph_galley = painter.layout_no_wrap(glyph.to_string(), font, color); let top_left = { let mut glyph_rect = glyph_galley.rect; glyph_rect.set_center(helper.get_animation_rect().center()); glyph_rect.left_top() }; painter.galley(top_left, glyph_galley, Color32::WHITE); helper.take_animation_response() } } fn glyph_options_ui( ui: &mut egui::Ui, font_size: f32, max_height: f32, glyphs: &[char], ) -> Option { let mut selected_glyph = None; egui::ScrollArea::vertical() .max_height(max_height) .show(ui, |ui| { let max_width = ui.available_width(); let mut row_glyphs = Vec::new(); let mut cur_width = 0.0; let spacing = ui.spacing().item_spacing.x; for (index, glyph) in glyphs.iter().enumerate() { let next_glyph_size = glyph_icon_max_size(ui, glyph, font_size); if cur_width + spacing + next_glyph_size.x > max_width { if let Some(selected) = paint_row(ui, &row_glyphs, font_size) { selected_glyph = Some(selected); } row_glyphs.clear(); cur_width = 0.0; } cur_width += spacing; cur_width += next_glyph_size.x; row_glyphs.push(*glyph); if index == glyphs.len() - 1 { if let Some(selected) = paint_row(ui, &row_glyphs, font_size) { selected_glyph = Some(selected); } } } }); selected_glyph } fn paint_row(ui: &mut egui::Ui, row_glyphs: &[char], font_size: f32) -> Option { let mut selected_glyph = None; ui.horizontal(|ui| { for glyph in row_glyphs { let glyph_size = glyph_icon_max_size(ui, glyph, font_size); if ui .add(glyph_icon( *glyph, font_size, glyph_size, ui.visuals().text_color(), )) .clicked() { selected_glyph = Some(*glyph); } } }); selected_glyph } mod preview { use crate::{ deck_state::DeckState, ui::{Preview, PreviewConfig}, }; use super::ConfigureDeckView; use notedeck::{App, AppContext}; pub struct ConfigureDeckPreview { state: DeckState, } impl ConfigureDeckPreview { fn new() -> Self { let state = DeckState::default(); ConfigureDeckPreview { state } } } impl App for ConfigureDeckPreview { fn update(&mut self, _app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { ConfigureDeckView::new(&mut self.state).ui(ui); } } impl Preview for ConfigureDeckView<'_> { type Prev = ConfigureDeckPreview; fn preview(_cfg: PreviewConfig) -> Self::Prev { ConfigureDeckPreview::new() } } }