notedeck

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

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 }