notedeck

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

commit a71e8206fb91a2b6c05e8bec77abc0a2e0e6bfb2
parent a5e1fbf3280e85b235e893e3d8eb06373c69447f
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 19 Apr 2024 14:09:19 -0700

introduce View and Previews traits

In this commit we refactor the preview mechanism, and switch to
responsive views by default.

To create a preview, your view now has to implement the Preview trait.
This is very similar to SwiftUI's preview mechanism.

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Msrc/account_login_view.rs | 220+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/app.rs | 7+------
Msrc/app_creation.rs | 2+-
Msrc/lib.rs | 2+-
Msrc/relay_view.rs | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/ui/mod.rs | 12++++++++++++
Asrc/ui/preview.rs | 33+++++++++++++++++++++++++++++++++
Dsrc/ui_preview/account_login_preview.rs | 39---------------------------------------
Dsrc/ui_preview/egui_preview_setup.rs | 15---------------
Msrc/ui_preview/main.rs | 109+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Dsrc/ui_preview/relay_view_preview.rs | 35-----------------------------------
11 files changed, 305 insertions(+), 276 deletions(-)

diff --git a/src/account_login_view.rs b/src/account_login_view.rs @@ -1,74 +1,142 @@ use crate::app_style::NotedeckTextStyle; use crate::key_parsing::{perform_key_retrieval, LoginError}; use crate::login_manager::LoginManager; +use crate::ui; +use crate::ui::{Preview, View}; use egui::{ Align, Align2, Button, Color32, Frame, Id, LayerId, Margin, Pos2, Rect, RichText, Rounding, Ui, Vec2, Window, }; use egui::{Image, TextBuffer, TextEdit}; -pub struct DesktopAccountLoginView<'a> { - ctx: &'a egui::Context, +pub struct AccountLoginView<'a> { manager: &'a mut LoginManager, generate_y_intercept: Option<f32>, } -impl<'a> DesktopAccountLoginView<'a> { - pub fn new(ctx: &'a egui::Context, manager: &'a mut LoginManager) -> Self { - DesktopAccountLoginView { - ctx, +impl<'a> View for AccountLoginView<'a> { + fn ui(&mut self, ui: &mut egui::Ui) { + if ui::is_mobile(ui.ctx()) { + self.show_mobile(ui); + } else { + self.show(ui); + } + } +} + +impl<'a> AccountLoginView<'a> { + pub fn new(manager: &'a mut LoginManager) -> Self { + AccountLoginView { manager, generate_y_intercept: None, } } - pub fn panel(&mut self) { - let frame = egui::CentralPanel::default(); - - let screen_width = self.ctx.screen_rect().max.x; - let screen_height = self.ctx.screen_rect().max.y; + fn show(&mut self, ui: &mut egui::Ui) -> egui::Response { + let screen_width = ui.ctx().screen_rect().max.x; + let screen_height = ui.ctx().screen_rect().max.y; - frame.show(self.ctx, |ui| { - let title_layer = LayerId::new(egui::Order::Background, Id::new("Title layer")); + let title_layer = LayerId::new(egui::Order::Background, Id::new("Title layer")); - let mut top_panel_height: Option<f32> = None; - ui.with_layer_id(title_layer, |ui| { - egui::TopBottomPanel::top("Top") - .resizable(false) - .default_height(340.0) - .frame(Frame::none()) - .show_separator_line(false) - .show_inside(ui, |ui| { - top_panel_height = Some(ui.available_rect_before_wrap().bottom()); - self.top_title_area(ui); - }); - }); - - egui::TopBottomPanel::bottom("Bottom") + let mut top_panel_height: Option<f32> = None; + ui.with_layer_id(title_layer, |ui| { + egui::TopBottomPanel::top("Top") .resizable(false) + .default_height(340.0) .frame(Frame::none()) .show_separator_line(false) .show_inside(ui, |ui| { - self.window(ui, top_panel_height.unwrap_or(0.0)); + top_panel_height = Some(ui.available_rect_before_wrap().bottom()); + self.top_title_area(ui); }); - - let top_rect = Rect { - min: Pos2::ZERO, - max: Pos2::new( - screen_width, - self.generate_y_intercept.unwrap_or(screen_height * 0.5), - ), - }; - - let top_background_color = ui.visuals().noninteractive().bg_fill; - ui.painter_at(top_rect) - .with_layer_id(LayerId::background()) - .rect_filled(top_rect, Rounding::ZERO, top_background_color); }); + + egui::TopBottomPanel::bottom("Bottom") + .resizable(false) + .frame(Frame::none()) + .show_separator_line(false) + .show_inside(ui, |ui| { + self.window(ui, top_panel_height.unwrap_or(0.0)); + }); + + let top_rect = Rect { + min: Pos2::ZERO, + max: Pos2::new( + screen_width, + self.generate_y_intercept.unwrap_or(screen_height * 0.5), + ), + }; + + let top_background_color = ui.visuals().noninteractive().bg_fill; + ui.painter_at(top_rect) + .with_layer_id(LayerId::background()) + .rect_filled(top_rect, Rounding::ZERO, top_background_color); + + egui::CentralPanel::default() + .show(ui.ctx(), |ui: &mut egui::Ui| {}) + .response + } + + fn mobile_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { + ui.vertical(|ui| { + ui.vertical_centered(|ui| { + ui.add(logo_unformatted().max_width(256.0)); + ui.add_space(64.0); + ui.label(login_info_text()); + ui.add_space(32.0); + ui.label(login_title_text()); + }); + + ui.horizontal(|ui| { + ui.label(login_textedit_info_text()); + }); + + ui.vertical_centered_justified(|ui| { + ui.add(login_textedit(&mut self.manager.login_key)); + + if ui.add(login_button()).clicked() { + self.manager.promise = Some(perform_key_retrieval(&self.manager.login_key)); + } + }); + + ui.horizontal(|ui| { + ui.label( + RichText::new("New to Nostr?") + .color(ui.style().visuals.noninteractive().fg_stroke.color) + .text_style(NotedeckTextStyle::Body.text_style()), + ); + + if ui + .add(Button::new(RichText::new("Create Account")).frame(false)) + .clicked() + { + // TODO: navigate to 'create account' screen + } + }); + }) + .response + } + + pub fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response { + egui::CentralPanel::default() + .show(ui.ctx(), |_| { + Window::new("Login") + .movable(true) + .constrain(true) + .collapsible(false) + .drag_to_scroll(false) + .title_bar(false) + .resizable(false) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .frame(Frame::central_panel(&ui.ctx().style())) + .max_width(ui.ctx().screen_rect().width() - 32.0) // margin + .show(ui.ctx(), |ui| self.mobile_ui(ui)); + }) + .response } fn window(&mut self, ui: &mut Ui, top_panel_height: f32) { - let needed_height_over_top = (self.ctx.screen_rect().bottom() / 2.0) - 230.0; + let needed_height_over_top = (ui.ctx().screen_rect().bottom() / 2.0) - 230.0; let y_offset = if top_panel_height > needed_height_over_top { top_panel_height - needed_height_over_top } else { @@ -274,67 +342,21 @@ fn login_textedit(text: &mut dyn TextBuffer) -> TextEdit { .margin(Margin::same(12.0)) } -pub struct MobileAccountLoginView<'a> { - ctx: &'a egui::Context, - manager: &'a mut LoginManager, +pub struct AccountLoginPreview { + manager: LoginManager, } -impl<'a> MobileAccountLoginView<'a> { - pub fn new(ctx: &'a egui::Context, manager: &'a mut LoginManager) -> Self { - MobileAccountLoginView { ctx, manager } +impl View for AccountLoginPreview { + fn ui(&mut self, ui: &mut egui::Ui) { + AccountLoginView::new(&mut self.manager).ui(ui); } +} - pub fn panel(&mut self) { - let frame = egui::CentralPanel::default(); - - frame.show(self.ctx, |_| { - Window::new("Login") - .movable(true) - .constrain(true) - .collapsible(false) - .drag_to_scroll(false) - .title_bar(false) - .resizable(false) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .frame(Frame::central_panel(&self.ctx.style())) - .max_width(self.ctx.screen_rect().width() - 32.0) // margin - .show(self.ctx, |ui| { - ui.vertical_centered(|ui| { - ui.add(logo_unformatted().max_width(256.0)); - ui.add_space(64.0); - ui.label(login_info_text()); - ui.add_space(32.0); - ui.label(login_title_text()); - }); - - ui.horizontal(|ui| { - ui.label(login_textedit_info_text()); - }); - - ui.vertical_centered_justified(|ui| { - ui.add(login_textedit(&mut self.manager.login_key)); +impl<'a> Preview for AccountLoginView<'a> { + type Prev = AccountLoginPreview; - if ui.add(login_button()).clicked() { - self.manager.promise = - Some(perform_key_retrieval(&self.manager.login_key)); - } - }); - - ui.horizontal(|ui| { - ui.label( - RichText::new("New to Nostr?") - .color(ui.style().visuals.noninteractive().fg_stroke.color) - .text_style(NotedeckTextStyle::Body.text_style()), - ); - - if ui - .add(Button::new(RichText::new("Create Account")).frame(false)) - .clicked() - { - // TODO: navigate to 'create account' screen - } - }); - }); - }); + fn preview() -> Self::Prev { + let manager = LoginManager::new(); + AccountLoginPreview { manager } } } diff --git a/src/app.rs b/src/app.rs @@ -6,6 +6,7 @@ use crate::imgcache::ImageCache; use crate::notecache::NoteCache; use crate::timeline; use crate::ui; +use crate::ui::is_mobile; use crate::Result; use egui::containers::scroll_area::ScrollBarVisibility; @@ -87,12 +88,6 @@ pub struct Damus { frame_history: crate::frame_history::FrameHistory, } -pub fn is_mobile(ctx: &egui::Context) -> bool { - //true - let screen_size = ctx.screen_rect().size(); - screen_size.x < 550.0 -} - fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) { let ctx = ctx.clone(); let wakeup = move || { diff --git a/src/app_creation.rs b/src/app_creation.rs @@ -1,6 +1,6 @@ -use crate::app::is_mobile; use crate::app_style::{create_text_styles, dark_mode, desktop_font_size, mobile_font_size}; use crate::fonts::setup_fonts; +use crate::ui::is_mobile; use eframe::NativeOptions; pub const UI_SCALE_FACTOR: f32 = 0.2; diff --git a/src/lib.rs b/src/lib.rs @@ -23,7 +23,7 @@ mod result; mod time; mod timecache; mod timeline; -mod ui; +pub mod ui; #[cfg(test)] #[macro_use] diff --git a/src/relay_view.rs b/src/relay_view.rs @@ -1,52 +1,52 @@ use crate::relay_pool_manager::{RelayPoolManager, RelayStatus}; +use crate::ui::{Preview, View}; use egui::{Align, Button, Frame, Layout, Margin, Rgba, RichText, Rounding, Ui, Vec2}; use crate::app_style::NotedeckTextStyle; +use enostr::RelayPool; pub struct RelayView<'a> { - ctx: &'a egui::Context, manager: RelayPoolManager<'a>, } -impl<'a> RelayView<'a> { - pub fn new(ctx: &'a egui::Context, manager: RelayPoolManager<'a>) -> Self { - RelayView { ctx, manager } - } +impl<'a> View for RelayView<'a> { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(24.0); - pub fn panel(&'a mut self) { - let mut indices_to_remove: Option<Vec<usize>> = None; + ui.horizontal(|ui| { + ui.with_layout(Layout::left_to_right(Align::Center), |ui| { + ui.label( + RichText::new("Relays").text_style(NotedeckTextStyle::Heading2.text_style()), + ); + }); - egui::CentralPanel::default().show(self.ctx, |ui| { - ui.add_space(24.0); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.add(add_relay_button()).clicked() { + // TODO: navigate to 'add relay view' + }; + }); + }); - ui.horizontal(|ui| { - ui.with_layout(Layout::left_to_right(Align::Center), |ui| { - ui.label( - RichText::new("Relays") - .text_style(NotedeckTextStyle::Heading2.text_style()), - ); - }); + ui.add_space(8.0); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.add(add_relay_button()).clicked() { - // TODO: navigate to 'add relay view' - }; - }); + egui::ScrollArea::vertical() + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + if let Some(indices) = self.show_relays(ui) { + self.manager.remove_relays(indices); + } }); + } +} - ui.add_space(8.0); - - egui::ScrollArea::vertical() - .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - indices_to_remove = self.show_relays(ui); - }); - }); +impl<'a> RelayView<'a> { + pub fn new(manager: RelayPoolManager<'a>) -> Self { + RelayView { manager } + } - if let Some(indices) = indices_to_remove { - self.manager.remove_relays(indices); - } + pub fn panel(&mut self, ui: &mut egui::Ui) { + egui::CentralPanel::default().show(ui.ctx(), |ui| self.ui(ui)); } /// Show the current relays, and returns the indices of relays the user requested to delete @@ -169,3 +169,44 @@ fn get_connection_icon(status: &RelayStatus) -> egui::Image<'static> { egui::Image::new(img_data) } + +// PREVIEWS + +pub struct RelayViewPreview { + pool: RelayPool, +} + +#[allow(unused_must_use)] +impl RelayViewPreview { + fn new() -> Self { + let mut pool = RelayPool::new(); + let wakeup = move || {}; + + pool.add_url("wss://relay.damus.io".to_string(), wakeup); + pool.add_url("wss://eden.nostr.land".to_string(), wakeup); + pool.add_url("wss://nostr.wine".to_string(), wakeup); + pool.add_url("wss://nos.lol".to_string(), wakeup); + pool.add_url("wss://test_relay_url_long_00000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(), wakeup); + + for _ in 0..20 { + pool.add_url("tmp".to_string(), wakeup); + } + + RelayViewPreview { pool } + } +} + +impl View for RelayViewPreview { + fn ui(&mut self, ui: &mut egui::Ui) { + self.pool.try_recv(); + RelayView::new(RelayPoolManager::new(&mut self.pool)).ui(ui) + } +} + +impl<'a> Preview for RelayView<'a> { + type Prev = RelayViewPreview; + + fn preview() -> Self::Prev { + RelayViewPreview::new() + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs @@ -1,11 +1,17 @@ pub mod note; +pub mod preview; pub mod username; pub use note::Note; +pub use preview::{Preview, PreviewApp}; pub use username::Username; use egui::Margin; +pub trait View { + fn ui(&mut self, ui: &mut egui::Ui); +} + pub fn padding<R>( amount: impl Into<Margin>, ui: &mut egui::Ui, @@ -15,3 +21,9 @@ pub fn padding<R>( .inner_margin(amount) .show(ui, add_contents) } + +pub fn is_mobile(ctx: &egui::Context) -> bool { + //true + let screen_size = ctx.screen_rect().size(); + screen_size.x < 550.0 +} diff --git a/src/ui/preview.rs b/src/ui/preview.rs @@ -0,0 +1,33 @@ +use crate::ui::View; + +pub trait Preview { + type Prev: View; + + fn preview() -> Self::Prev; +} + +pub struct PreviewApp { + view: Box<dyn View>, +} + +impl<V> From<V> for PreviewApp +where + V: View + 'static, +{ + fn from(v: V) -> Self { + PreviewApp::new(v) + } +} + +impl PreviewApp { + pub fn new(view: impl View + 'static) -> PreviewApp { + let view = Box::new(view); + Self { view } + } +} + +impl eframe::App for PreviewApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| self.view.ui(ui)); + } +} diff --git a/src/ui_preview/account_login_preview.rs b/src/ui_preview/account_login_preview.rs @@ -1,39 +0,0 @@ -use crate::egui_preview_setup::{EguiPreviewCase, EguiPreviewSetup}; -use notedeck::account_login_view::{DesktopAccountLoginView, MobileAccountLoginView}; -use notedeck::login_manager::LoginManager; - -pub struct DesktopAccountLoginPreview { - manager: LoginManager, -} - -impl EguiPreviewCase for DesktopAccountLoginPreview { - fn new(_supr: EguiPreviewSetup) -> Self { - DesktopAccountLoginPreview { - manager: LoginManager::new(), - } - } -} - -impl eframe::App for DesktopAccountLoginPreview { - fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { - DesktopAccountLoginView::new(ctx, &mut self.manager).panel() - } -} - -pub struct MobileAccountLoginPreview { - manager: LoginManager, -} - -impl EguiPreviewCase for MobileAccountLoginPreview { - fn new(_supr: EguiPreviewSetup) -> Self { - MobileAccountLoginPreview { - manager: LoginManager::new(), - } - } -} - -impl eframe::App for MobileAccountLoginPreview { - fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { - MobileAccountLoginView::new(ctx, &mut self.manager).panel() - } -} diff --git a/src/ui_preview/egui_preview_setup.rs b/src/ui_preview/egui_preview_setup.rs @@ -1,15 +0,0 @@ -use notedeck::app_creation::setup_cc; - -pub struct EguiPreviewSetup {} - -pub trait EguiPreviewCase: eframe::App { - fn new(supr: EguiPreviewSetup) -> Self; -} - -impl EguiPreviewSetup { - pub fn new(cc: &eframe::CreationContext<'_>) -> Self { - setup_cc(cc); - - EguiPreviewSetup {} - } -} diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs @@ -1,57 +1,72 @@ -mod account_login_preview; -mod egui_preview_setup; -mod relay_view_preview; -use account_login_preview::{DesktopAccountLoginPreview, MobileAccountLoginPreview}; -use egui_preview_setup::{EguiPreviewCase, EguiPreviewSetup}; -use notedeck::app_creation::{generate_mobile_emulator_native_options, generate_native_options}; -use relay_view_preview::RelayViewPreview; +use notedeck::account_login_view::AccountLoginView; +use notedeck::app_creation::{ + generate_mobile_emulator_native_options, generate_native_options, setup_cc, +}; +use notedeck::relay_view::RelayView; +use notedeck::ui::{Preview, PreviewApp}; use std::env; -#[cfg(not(target_arch = "wasm32"))] +struct PreviewRunner { + force_mobile: bool, +} + +impl PreviewRunner { + fn new(force_mobile: bool) -> Self { + PreviewRunner { force_mobile } + } + + async fn run<P>(self, preview: P) + where + P: Into<PreviewApp> + 'static, + { + tracing_subscriber::fmt::init(); + + let native_options = if self.force_mobile { + generate_mobile_emulator_native_options() + } else { + generate_native_options() + }; + + let _ = eframe::run_native( + "UI Preview Runner", + native_options, + Box::new(|cc| { + setup_cc(cc); + Box::new(Into::<PreviewApp>::into(preview)) + }), + ); + } +} + #[tokio::main] -async fn run_test_app<F, T, O>(create_supr: F, create_child: O, is_mobile: bool) -where - F: 'static + FnOnce(&eframe::CreationContext<'_>) -> EguiPreviewSetup, - T: 'static + EguiPreviewCase, - O: 'static + FnOnce(EguiPreviewSetup) -> T, -{ - tracing_subscriber::fmt::init(); - - let native_options = if is_mobile { - generate_mobile_emulator_native_options() +async fn main() { + let mut name: Option<String> = None; + let mut is_mobile = false; + + for arg in env::args() { + if arg == "--mobile" { + is_mobile = true; + } else { + name = Some(arg); + } + } + + let name = if let Some(name) = name { + name } else { - generate_native_options() + println!("Please specify a component to test"); + return; }; - let _ = eframe::run_native( - "UI Preview Runner", - native_options, - Box::new(|cc| Box::new(create_child(create_supr(cc)))), - ); -} + let runner = PreviewRunner::new(is_mobile); -fn main() { - let args: Vec<String> = env::args().collect(); - - if args.len() > 1 { - match args[1].as_str() { - "DesktopAccountLoginPreview" => run_test_app( - EguiPreviewSetup::new, - DesktopAccountLoginPreview::new, - false, - ), - "MobileAccountLoginPreview" => { - run_test_app(EguiPreviewSetup::new, MobileAccountLoginPreview::new, true) - } - "DesktopRelayViewPreview" => { - run_test_app(EguiPreviewSetup::new, RelayViewPreview::new, false) - } - "MobileRelayViewPreview" => { - run_test_app(EguiPreviewSetup::new, RelayViewPreview::new, true) - } - _ => println!("Component not found."), + match name.as_ref() { + "AccountLoginView" => { + runner.run(AccountLoginView::preview()).await; } - } else { - println!("Please specify a component to test."); + "RelayView" => { + runner.run(RelayView::preview()).await; + } + _ => println!("Component not found."), } } diff --git a/src/ui_preview/relay_view_preview.rs b/src/ui_preview/relay_view_preview.rs @@ -1,35 +0,0 @@ -use enostr::RelayPool; -use notedeck::{relay_pool_manager::RelayPoolManager, relay_view::RelayView}; - -use crate::egui_preview_setup::{EguiPreviewCase, EguiPreviewSetup}; - -pub struct RelayViewPreview { - pool: RelayPool, -} - -#[allow(unused_must_use)] -impl EguiPreviewCase for RelayViewPreview { - fn new(_supr: EguiPreviewSetup) -> Self { - let mut pool = RelayPool::new(); - let wakeup = move || {}; - - pool.add_url("wss://relay.damus.io".to_string(), wakeup); - pool.add_url("wss://eden.nostr.land".to_string(), wakeup); - pool.add_url("wss://nostr.wine".to_string(), wakeup); - pool.add_url("wss://nos.lol".to_string(), wakeup); - pool.add_url("wss://test_relay_url_long_00000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(), wakeup); - - for _ in 0..20 { - pool.add_url("tmp".to_string(), wakeup); - } - - RelayViewPreview { pool } - } -} - -impl eframe::App for RelayViewPreview { - fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { - self.pool.try_recv(); - RelayView::new(ctx, RelayPoolManager::new(&mut self.pool)).panel(); - } -}