ConfigureDeck & EditDeck user interfaces
`./preview ConfigureDeckView` `./preview EditDeckView` Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
65
src/deck_state.rs
Normal file
65
src/deck_state.rs
Normal file
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,8 @@ mod app_style;
|
||||
mod args;
|
||||
mod colors;
|
||||
mod column;
|
||||
mod deck_state;
|
||||
mod decks;
|
||||
mod draft;
|
||||
mod filter;
|
||||
mod fonts;
|
||||
|
||||
324
src/ui/configure_deck.rs
Normal file
324
src/ui/configure_deck.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/ui/edit_deck.rs
Normal file
91
src/ui/edit_deck.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user