notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }