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:
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();
- }
-}