notedeck

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

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 }