commit 35613f2e74961f1123a3883c665d27095467a81a
parent 83fe173ba3490df663ba8f729a0c23ccd30f00ca
Author: kernelkind <kernelkind@gmail.com>
Date: Thu, 5 Dec 2024 17:42:21 -0500
ConfigureDeck & EditDeck user interfaces
`./preview ConfigureDeckView`
`./preview EditDeckView`
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
6 files changed, 488 insertions(+), 0 deletions(-)
diff --git a/src/deck_state.rs b/src/deck_state.rs
@@ -0,0 +1,65 @@
+use crate::{app_style::emoji_font_family, decks::Deck};
+
+/// State for UI creating/editing deck
+pub struct DeckState {
+ pub deck_name: String,
+ pub selected_glyph: Option<char>,
+ pub deleting: bool,
+ pub selecting_glyph: bool,
+ pub warn_no_title: bool,
+ pub warn_no_icon: bool,
+ glyph_options: Option<Vec<char>>,
+}
+
+impl DeckState {
+ pub fn load(&mut self, deck: &Deck) {
+ self.deck_name = deck.name.clone();
+ self.selected_glyph = Some(deck.icon);
+ }
+
+ pub fn from_deck(deck: &Deck) -> Self {
+ let deck_name = deck.name.clone();
+ let selected_glyph = Some(deck.icon);
+ Self {
+ deck_name,
+ selected_glyph,
+ ..Default::default()
+ }
+ }
+
+ pub fn clear(&mut self) {
+ *self = Default::default();
+ }
+
+ pub fn get_glyph_options(&mut self, ui: &egui::Ui) -> &Vec<char> {
+ self.glyph_options
+ .get_or_insert_with(|| available_characters(ui, emoji_font_family()))
+ }
+}
+
+impl Default for DeckState {
+ fn default() -> Self {
+ Self {
+ deck_name: Default::default(),
+ selected_glyph: Default::default(),
+ deleting: Default::default(),
+ selecting_glyph: true,
+ warn_no_icon: Default::default(),
+ warn_no_title: Default::default(),
+ glyph_options: Default::default(),
+ }
+ }
+}
+
+fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> Vec<char> {
+ ui.fonts(|f| {
+ f.lock()
+ .fonts
+ .font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
+ .characters()
+ .iter()
+ .filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
+ .copied()
+ .collect()
+ })
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -12,6 +12,8 @@ mod app_style;
mod args;
mod colors;
mod column;
+mod deck_state;
+mod decks;
mod draft;
mod filter;
mod fonts;
diff --git a/src/ui/configure_deck.rs b/src/ui/configure_deck.rs
@@ -0,0 +1,324 @@
+use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget};
+
+use crate::{
+ app_style::{deck_icon_font_sized, get_font_size, NotedeckTextStyle},
+ colors::PINK,
+ deck_state::DeckState,
+ fonts::NamedFontFamily,
+};
+
+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<ConfigureDeckResponse> {
+ let title_font = egui::FontId::new(
+ 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(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::<Vec<_>>()
+ .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 + use<'_> {
+ 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<char>,
+ 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) -> 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, Color32::WHITE);
+
+ 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<char> {
+ 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<char> {
+ 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)).clicked() {
+ selected_glyph = Some(*glyph);
+ }
+ }
+ });
+ selected_glyph
+}
+
+mod preview {
+ use crate::{
+ deck_state::DeckState,
+ ui::{Preview, PreviewConfig, View},
+ };
+
+ use super::ConfigureDeckView;
+
+ pub struct ConfigureDeckPreview {
+ state: DeckState,
+ }
+
+ impl ConfigureDeckPreview {
+ fn new() -> Self {
+ let state = DeckState::default();
+
+ ConfigureDeckPreview { state }
+ }
+ }
+
+ impl View for ConfigureDeckPreview {
+ fn ui(&mut self, 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()
+ }
+ }
+}
diff --git a/src/ui/edit_deck.rs b/src/ui/edit_deck.rs
@@ -0,0 +1,91 @@
+use egui::Widget;
+
+use crate::deck_state::DeckState;
+
+use super::{
+ configure_deck::{ConfigureDeckResponse, ConfigureDeckView},
+ padding,
+};
+
+pub struct EditDeckView<'a> {
+ config_view: ConfigureDeckView<'a>,
+}
+
+static EDIT_TEXT: &str = "Edit Deck";
+
+pub enum EditDeckResponse {
+ Edit(ConfigureDeckResponse),
+ Delete,
+}
+
+impl<'a> EditDeckView<'a> {
+ pub fn new(state: &'a mut DeckState) -> Self {
+ let config_view = ConfigureDeckView::new(state).with_create_text(EDIT_TEXT);
+ Self { config_view }
+ }
+
+ pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<EditDeckResponse> {
+ let mut edit_deck_resp = None;
+
+ padding(egui::Margin::symmetric(16.0, 4.0), ui, |ui| {
+ if ui.add(delete_button()).clicked() {
+ edit_deck_resp = Some(EditDeckResponse::Delete);
+ }
+ });
+
+ if let Some(config_resp) = self.config_view.ui(ui) {
+ edit_deck_resp = Some(EditDeckResponse::Edit(config_resp))
+ }
+
+ edit_deck_resp
+ }
+}
+
+fn delete_button() -> impl Widget {
+ |ui: &mut egui::Ui| {
+ let size = egui::vec2(108.0, 40.0);
+ ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| {
+ ui.add(
+ egui::Button::new("Delete Deck")
+ .fill(ui.visuals().error_fg_color)
+ .min_size(size),
+ )
+ })
+ .inner
+ }
+}
+
+mod preview {
+ use crate::{
+ deck_state::DeckState,
+ ui::{Preview, PreviewConfig, View},
+ };
+
+ use super::EditDeckView;
+
+ pub struct EditDeckPreview {
+ state: DeckState,
+ }
+
+ impl EditDeckPreview {
+ fn new() -> Self {
+ let state = DeckState::default();
+
+ EditDeckPreview { state }
+ }
+ }
+
+ impl View for EditDeckPreview {
+ fn ui(&mut self, ui: &mut egui::Ui) {
+ EditDeckView::new(&mut self.state).ui(ui);
+ }
+ }
+
+ impl Preview for EditDeckView<'_> {
+ type Prev = EditDeckPreview;
+
+ fn preview(_cfg: PreviewConfig) -> Self::Prev {
+ EditDeckPreview::new()
+ }
+ }
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
@@ -3,6 +3,8 @@ pub mod accounts;
pub mod add_column;
pub mod anim;
pub mod column;
+pub mod configure_deck;
+pub mod edit_deck;
pub mod mention;
pub mod note;
pub mod preview;
diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs
@@ -1,3 +1,5 @@
+use notedeck::ui::configure_deck::ConfigureDeckView;
+use notedeck::ui::edit_deck::EditDeckView;
use notedeck::ui::{
account_login_view::AccountLoginView, accounts::AccountsView, add_column::AddColumnView,
DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview,
@@ -106,5 +108,7 @@ async fn main() {
DesktopSidePanel,
PostView,
AddColumnView,
+ ConfigureDeckView,
+ EditDeckView,
);
}