From 12075d42434d0ce16cfc1578ed58ad3525193211 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Tue, 16 Apr 2024 12:41:14 -0600 Subject: [PATCH] New Widgets: Manually port important changes from 'feature/color-palette' branch --- assets/magnifyingglass.svg | 3 + gossip-bin/src/ui/assets.rs | 68 +++ gossip-bin/src/ui/feed/mod.rs | 8 +- gossip-bin/src/ui/help/mod.rs | 3 - gossip-bin/src/ui/help/theme.rs | 145 ------ gossip-bin/src/ui/mod.rs | 71 ++- .../src/ui/notifications/auth_request.rs | 3 +- .../src/ui/notifications/conn_request.rs | 3 +- gossip-bin/src/ui/notifications/mod.rs | 1 - .../src/ui/notifications/nip46_request.rs | 9 +- gossip-bin/src/ui/people/list.rs | 13 +- gossip-bin/src/ui/people/person.rs | 4 +- gossip-bin/src/ui/relays/active.rs | 4 +- gossip-bin/src/ui/relays/known.rs | 4 +- gossip-bin/src/ui/relays/mine.rs | 4 +- gossip-bin/src/ui/relays/mod.rs | 12 +- gossip-bin/src/ui/theme/mod.rs | 2 + gossip-bin/src/ui/theme/test_page.rs | 441 ++++++++++++++++++ gossip-bin/src/ui/widgets/button.rs | 435 ++++++++++++++++- gossip-bin/src/ui/widgets/mod.rs | 53 +-- gossip-bin/src/ui/widgets/more_menu.rs | 4 +- gossip-bin/src/ui/widgets/relay_entry.rs | 2 +- gossip-bin/src/ui/widgets/switch.rs | 282 +++++++++-- gossip-bin/src/ui/widgets/textedit.rs | 185 +++++++- 24 files changed, 1433 insertions(+), 326 deletions(-) create mode 100755 assets/magnifyingglass.svg create mode 100644 gossip-bin/src/ui/assets.rs delete mode 100644 gossip-bin/src/ui/help/theme.rs create mode 100644 gossip-bin/src/ui/theme/test_page.rs diff --git a/assets/magnifyingglass.svg b/assets/magnifyingglass.svg new file mode 100755 index 00000000..cfaafe42 --- /dev/null +++ b/assets/magnifyingglass.svg @@ -0,0 +1,3 @@ + + + diff --git a/gossip-bin/src/ui/assets.rs b/gossip-bin/src/ui/assets.rs new file mode 100644 index 00000000..f505d17a --- /dev/null +++ b/gossip-bin/src/ui/assets.rs @@ -0,0 +1,68 @@ +use egui_winit::egui::{ColorImage, Context, TextureHandle, TextureOptions}; +use tiny_skia::Transform; +use usvg::TreeParsing; + +pub const SVG_OVERSAMPLE: f32 = 2.0; + +pub struct Assets { + pub options_symbol: TextureHandle, + pub magnifyingglass_symbol: TextureHandle, +} + +impl Assets { + pub fn init(ctx: &Context) -> Self { + // how to load an svg + let ppt = ctx.pixels_per_point(); + let dpi = ppt * 72.0; + let options_symbol = { + let bytes = include_bytes!("../../../assets/option.svg"); + let opt = usvg::Options { + dpi, + ..Default::default() + }; + let rtree = usvg::Tree::from_data(bytes, &opt).unwrap(); + let [w, h] = [ + (rtree.size.width() * ppt * SVG_OVERSAMPLE) as u32, + (rtree.size.height() * ppt * SVG_OVERSAMPLE) as u32, + ]; + let mut pixmap = tiny_skia::Pixmap::new(w, h).unwrap(); + let tree = resvg::Tree::from_usvg(&rtree); + tree.render( + Transform::from_scale(ppt * SVG_OVERSAMPLE, ppt * SVG_OVERSAMPLE), + &mut pixmap.as_mut(), + ); + let color_image = ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data()); + ctx.load_texture("options_symbol", color_image, TextureOptions::LINEAR) + }; + + let magnifyingglass_symbol = { + let bytes = include_bytes!("../../../assets/magnifyingglass.svg"); + let opt = usvg::Options { + dpi, + ..Default::default() + }; + let rtree = usvg::Tree::from_data(bytes, &opt).unwrap(); + let [w, h] = [ + (rtree.size.width() * ppt * SVG_OVERSAMPLE) as u32, + (rtree.size.height() * ppt * SVG_OVERSAMPLE) as u32, + ]; + let mut pixmap = tiny_skia::Pixmap::new(w, h).unwrap(); + let tree = resvg::Tree::from_usvg(&rtree); + tree.render( + Transform::from_scale(ppt * SVG_OVERSAMPLE, ppt * SVG_OVERSAMPLE), + &mut pixmap.as_mut(), + ); + let color_image = ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data()); + ctx.load_texture( + "magnifyingglass_symbol", + color_image, + TextureOptions::LINEAR, + ) + }; + + Self { + magnifyingglass_symbol, + options_symbol, + } + } +} diff --git a/gossip-bin/src/ui/feed/mod.rs b/gossip-bin/src/ui/feed/mod.rs index 2b33b9fb..4f6d8ea6 100644 --- a/gossip-bin/src/ui/feed/mod.rs +++ b/gossip-bin/src/ui/feed/mod.rs @@ -104,8 +104,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, ui: &mut Ui) { ui.add_space(10.0); ui.label(RichText::new("Include replies").size(11.0)); - let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - if widgets::switch_with_size(ui, &mut app.mainfeed_include_nonroot, size) + if widgets::Switch::small(&app.theme, &mut app.mainfeed_include_nonroot) + .show(ui) .clicked() { app.set_page( @@ -150,8 +150,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, ui: &mut Ui) { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_space(16.0); ui.label(RichText::new("Everything").size(11.0)); - let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - if widgets::switch_with_size(ui, &mut app.inbox_include_indirect, size) + if widgets::Switch::small(&app.theme, &mut app.inbox_include_indirect) + .show(ui) .clicked() { app.set_page( diff --git a/gossip-bin/src/ui/help/mod.rs b/gossip-bin/src/ui/help/mod.rs index 297a4720..a91a7c02 100644 --- a/gossip-bin/src/ui/help/mod.rs +++ b/gossip-bin/src/ui/help/mod.rs @@ -5,7 +5,6 @@ use gossip_lib::PersonList; mod about; mod stats; -mod theme; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { if app.page == Page::HelpHelp { @@ -81,7 +80,5 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra stats::update(app, ctx, _frame, ui); } else if app.page == Page::HelpAbout { about::update(app, ctx, _frame, ui); - } else if app.page == Page::HelpTheme { - theme::update(app, ctx, _frame, ui); } } diff --git a/gossip-bin/src/ui/help/theme.rs b/gossip-bin/src/ui/help/theme.rs deleted file mode 100644 index daf40caf..00000000 --- a/gossip-bin/src/ui/help/theme.rs +++ /dev/null @@ -1,145 +0,0 @@ -use super::GossipUi; -use crate::ui::feed::NoteRenderData; -use crate::ui::HighlightType; -use eframe::egui; -use egui::text::LayoutJob; -use egui::widget_text::WidgetText; -use egui::{Color32, Context, Frame, Margin, RichText, Ui}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Background { - None, - Input, - Note, - HighlightedNote, -} - -pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - ui.add_space(10.0); - ui.heading("Theme Test".to_string()); - ui.add_space(12.0); - ui.separator(); - - // On No Background - Frame::none() - .inner_margin(Margin::symmetric(20.0, 20.0)) - .show(ui, |ui| { - ui.heading("No Background"); - inner(app, ui, Background::None); - }); - - // On Note Background - let render_data = NoteRenderData { - height: 200.0, - is_new: false, - is_main_event: false, - has_repost: false, - is_comment_mention: false, - is_thread: false, - is_first: true, - is_last: true, - thread_position: 0, - }; - Frame::none() - .inner_margin(app.theme.feed_frame_inner_margin(&render_data)) - .outer_margin(app.theme.feed_frame_outer_margin(&render_data)) - .rounding(app.theme.feed_frame_rounding(&render_data)) - .shadow(app.theme.feed_frame_shadow(&render_data)) - .fill(app.theme.feed_frame_fill(&render_data)) - .stroke(app.theme.feed_frame_stroke(&render_data)) - .show(ui, |ui| { - ui.heading("Note Background"); - ui.label("(with note margins)"); - inner(app, ui, Background::Note); - }); - - // On Highlighted Note Background - let render_data = NoteRenderData { - height: 200.0, - is_new: true, - is_main_event: false, - has_repost: false, - is_comment_mention: false, - is_thread: false, - is_first: true, - is_last: true, - thread_position: 0, - }; - Frame::none() - .inner_margin(app.theme.feed_frame_inner_margin(&render_data)) - .outer_margin(app.theme.feed_frame_outer_margin(&render_data)) - .rounding(app.theme.feed_frame_rounding(&render_data)) - .shadow(app.theme.feed_frame_shadow(&render_data)) - .fill(app.theme.feed_frame_fill(&render_data)) - .stroke(app.theme.feed_frame_stroke(&render_data)) - .show(ui, |ui| { - ui.heading("Unread Note Background"); - ui.label("(with note margins)"); - inner(app, ui, Background::HighlightedNote); - }); - - // On Input Background - Frame::none() - .fill(app.theme.get_style().visuals.extreme_bg_color) - .inner_margin(Margin::symmetric(20.0, 20.0)) - .show(ui, |ui| { - ui.heading("Input Background"); - inner(app, ui, Background::Input); - }); -} - -fn inner(app: &mut GossipUi, ui: &mut Ui, background: Background) { - let theme = app.theme; - let accent = RichText::new("accent").color(theme.accent_color()); - let accent_complementary = RichText::new("accent complimentary (indirectly used)") - .color(theme.accent_complementary_color()); - - line(ui, accent); - line(ui, accent_complementary); - - if background == Background::Input { - for (ht, txt) in [ - (HighlightType::Nothing, "nothing"), - (HighlightType::PublicKey, "public key"), - (HighlightType::Event, "event"), - (HighlightType::Relay, "relay"), - (HighlightType::Hyperlink, "hyperlink"), - ] { - let mut highlight_job = LayoutJob::default(); - highlight_job.append( - &format!("highlight text format for {}", txt), - 0.0, - theme.highlight_text_format(ht), - ); - line(ui, WidgetText::LayoutJob(highlight_job)); - } - } - - if background == Background::Note || background == Background::HighlightedNote { - let warning_marker = - RichText::new("warning marker").color(theme.warning_marker_text_color()); - line(ui, warning_marker); - - let notice_marker = RichText::new("notice marker").color(theme.notice_marker_text_color()); - line(ui, notice_marker); - } - - if background != Background::Input { - ui.horizontal(|ui| { - ui.label(RichText::new("•").color(Color32::from_gray(128))); - crate::ui::widgets::break_anywhere_hyperlink_to( - ui, - "https://hyperlink.example.com", - "https://hyperlink.example.com", - ); - }); - } -} - -fn line(ui: &mut Ui, label: impl Into) { - let bullet = RichText::new("•").color(Color32::from_gray(128)); - ui.horizontal(|ui| { - ui.label(bullet); - ui.label(label); - }); -} diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 09374a7f..488bffed 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1,6 +1,6 @@ macro_rules! text_edit_line { ($app:ident, $var:expr) => { - crate::ui::widgets::TextEdit::singleline(&mut $var) + crate::ui::widgets::TextEdit::singleline(&$app.theme, &mut $var) .text_color($app.theme.input_text_color()) }; } @@ -33,6 +33,7 @@ macro_rules! write_setting { }; } +mod assets; mod components; mod dm_chat_list; mod feed; @@ -76,9 +77,9 @@ use std::hash::Hash; use std::rc::Rc; use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; -use usvg::TreeParsing; use zeroize::Zeroize; +use self::assets::Assets; use self::feed::Notes; use self::notifications::NotificationData; use self::widgets::NavItem; @@ -157,7 +158,8 @@ enum Page { HelpHelp, HelpStats, HelpAbout, - HelpTheme, + #[allow(unused)] + ThemeTest, Wizard(WizardPage), } @@ -204,7 +206,7 @@ impl Page { Page::HelpHelp => (SubMenu::Help.as_str(), "Troubleshooting".into()), Page::HelpStats => (SubMenu::Help.as_str(), "Stats".into()), Page::HelpAbout => (SubMenu::Help.as_str(), "About".into()), - Page::HelpTheme => (SubMenu::Help.as_str(), "Theme Test".into()), + Page::ThemeTest => (SubMenu::Help.as_str(), "Theme Test".into()), Page::Wizard(wp) => ("Wizard", wp.as_str().to_string()), } } @@ -433,10 +435,10 @@ struct GossipUi { feeds: feed::Feeds, // General Data + assets: Assets, about: About, icon: TextureHandle, placeholder_avatar: TextureHandle, - options_symbol: TextureHandle, unsaved_settings: UnsavedSettings, theme: Theme, avatars: HashMap, @@ -508,6 +510,8 @@ struct GossipUi { wizard_state: WizardState, + theme_test: crate::ui::theme::test_page::ThemeTest, + // Cached DM Channels dm_channel_cache: Vec, dm_channel_next_refresh: Instant, @@ -544,6 +548,9 @@ impl GossipUi { submenu_ids.insert(SubMenu::Relays, egui::Id::new(SubMenu::Relays.as_id_str())); submenu_ids.insert(SubMenu::Help, egui::Id::new(SubMenu::Help.as_id_str())); + // load Assets, but load again when DPI changes + let assets = Assets::init(&cctx.egui_ctx); + let icon_texture_handle = { let bytes = include_bytes!("../../../logo/gossip.png"); let image = image::load_from_memory(bytes).unwrap(); @@ -594,23 +601,6 @@ impl GossipUi { None => (false, (cctx.egui_ctx.pixels_per_point() * 72.0) as u32), }; - // how to load an svg (TODO do again when DPI changes) - let options_symbol = { - let bytes = include_bytes!("../../../assets/option.svg"); - let opt = usvg::Options { - dpi: override_dpi_value as f32, - ..Default::default() - }; - let rtree = usvg::Tree::from_data(bytes, &opt).unwrap(); - let [w, h] = [20_u32, 20_u32]; - let mut pixmap = tiny_skia::Pixmap::new(w, h).unwrap(); - let tree = resvg::Tree::from_usvg(&rtree); - tree.render(Default::default(), &mut pixmap.as_mut()); - let color_image = ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data()); - cctx.egui_ctx - .load_texture("options_symbol", color_image, TextureOptions::LINEAR) - }; - let mainfeed_include_nonroot = cctx .egui_ctx .data_mut(|d| d.get_persisted(egui::Id::new("mainfeed_include_nonroot"))) @@ -681,10 +671,11 @@ impl GossipUi { submenu_ids, settings_tab: SettingsTab::Id, feeds: feed::Feeds::default(), + // load Assets, but load again when DPI changes + assets, about: About::new(), icon: icon_texture_handle, placeholder_avatar: placeholder_avatar_texture_handle, - options_symbol, unsaved_settings: UnsavedSettings::load(), theme, avatars: HashMap::new(), @@ -731,6 +722,7 @@ impl GossipUi { zap_state: ZapState::None, note_being_zapped: None, wizard_state, + theme_test: Default::default(), dm_channel_cache: vec![], dm_channel_next_refresh: Instant::now(), dm_channel_error: None, @@ -762,21 +754,8 @@ impl GossipUi { // 'original' refers to 'before the user changes it in settings' self.original_dpi_value = self.override_dpi_value; - // load SVG's again when DPI changes - self.options_symbol = { - let bytes = include_bytes!("../../../assets/option.svg"); - let opt = usvg::Options { - dpi: self.override_dpi_value as f32, - ..Default::default() - }; - let rtree = usvg::Tree::from_data(bytes, &opt).unwrap(); - let [w, h] = [20_u32, 20_u32]; - let mut pixmap = tiny_skia::Pixmap::new(w, h).unwrap(); - let tree = resvg::Tree::from_usvg(&rtree); - tree.render(Default::default(), &mut pixmap.as_mut()); - let color_image = ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data()); - ctx.load_texture("options_symbol", color_image, TextureOptions::LINEAR) - }; + // Reload Assets when DPI changes + self.assets = Assets::init(ctx); // Set global pixels_per_point_times_100, used for image scaling. // this would warrant reloading images but the user experience isn't great as @@ -900,7 +879,7 @@ impl GossipUi { Page::Settings => { self.close_all_menus_except_feeds(ctx); } - Page::HelpHelp | Page::HelpStats | Page::HelpAbout | Page::HelpTheme => { + Page::HelpHelp | Page::HelpStats | Page::HelpAbout => { self.open_menu(ctx, SubMenu::Help); } _ => { @@ -1130,11 +1109,18 @@ impl GossipUi { self.add_menu_item_page(ui, Page::HelpHelp, None, true); self.add_menu_item_page(ui, Page::HelpStats, None, true); self.add_menu_item_page(ui, Page::HelpAbout, None, true); - self.add_menu_item_page(ui, Page::HelpTheme, None, true); }); self.after_openable_menu(ui, &cstate); } + #[cfg(debug_assertions)] + if self + .add_selected_label(ui, self.page == Page::ThemeTest, "Theme Test") + .clicked() + { + self.set_page(ctx, Page::ThemeTest); + } + // -- Status Area ui.with_layout(Layout::bottom_up(Align::LEFT), |ui| { notifications::draw_icons(self, ui); @@ -1520,9 +1506,10 @@ impl eframe::App for GossipUi { | Page::RelaysKnownNetwork(_) => relays::update(self, ctx, frame, ui), Page::Search => search::update(self, ctx, frame, ui), Page::Settings => settings::update(self, ctx, frame, ui), - Page::HelpHelp | Page::HelpStats | Page::HelpAbout | Page::HelpTheme => { + Page::HelpHelp | Page::HelpStats | Page::HelpAbout => { help::update(self, ctx, frame, ui) } + Page::ThemeTest => theme::test_page::update(self, ctx, frame, ui), Page::Wizard(_) => unreachable!(), } }); @@ -2166,7 +2153,7 @@ fn force_login(app: &mut GossipUi, ctx: &Context) { ui.add_space(16.0); } - let output = widgets::TextEdit::singleline(&mut app.password) + let output = widgets::TextEdit::singleline(&app.theme,&mut app.password) .password(true) .with_paste() .desired_width( 400.0) diff --git a/gossip-bin/src/ui/notifications/auth_request.rs b/gossip-bin/src/ui/notifications/auth_request.rs index 2c2945a2..eb1af54a 100644 --- a/gossip-bin/src/ui/notifications/auth_request.rs +++ b/gossip-bin/src/ui/notifications/auth_request.rs @@ -108,7 +108,8 @@ impl<'a> Notification<'a> for AuthRequest { }); ui.add_space(10.0); ui.label("Remember"); - widgets::switch_with_size(ui, &mut self.remember, super::SWITCH_SIZE) + widgets::Switch::large(theme, &mut self.remember) + .show(ui) .on_hover_text("store permission permanently"); }); }); diff --git a/gossip-bin/src/ui/notifications/conn_request.rs b/gossip-bin/src/ui/notifications/conn_request.rs index 6f500827..f14000f2 100644 --- a/gossip-bin/src/ui/notifications/conn_request.rs +++ b/gossip-bin/src/ui/notifications/conn_request.rs @@ -144,7 +144,8 @@ impl<'a> Notification<'a> for ConnRequest { }); ui.add_space(10.0); ui.label("Remember"); - widgets::switch_with_size(ui, &mut self.remember, super::SWITCH_SIZE) + widgets::Switch::large(theme, &mut self.remember) + .show(ui) .on_hover_text("store permission permanently"); }); }); diff --git a/gossip-bin/src/ui/notifications/mod.rs b/gossip-bin/src/ui/notifications/mod.rs index 351a9e99..13141a94 100644 --- a/gossip-bin/src/ui/notifications/mod.rs +++ b/gossip-bin/src/ui/notifications/mod.rs @@ -55,7 +55,6 @@ pub trait Notification<'a> { } type NotificationHandle = Rc Notification<'handle>>>; -const SWITCH_SIZE: Vec2 = Vec2 { x: 40.0, y: 20.0 }; pub struct NotificationData { active: Vec, diff --git a/gossip-bin/src/ui/notifications/nip46_request.rs b/gossip-bin/src/ui/notifications/nip46_request.rs index 327d5954..403b7353 100644 --- a/gossip-bin/src/ui/notifications/nip46_request.rs +++ b/gossip-bin/src/ui/notifications/nip46_request.rs @@ -131,12 +131,9 @@ impl<'a> Notification<'a> for Nip46Request { }); ui.add_space(10.0); ui.label("Remember"); - widgets::switch_with_size( - ui, - &mut self.remember, - super::SWITCH_SIZE, - ) - .on_hover_text("store permission permanently"); + widgets::Switch::large(theme, &mut self.remember) + .show(ui) + .on_hover_text("store permission permanently"); }); }); }); diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index a7e28413..a20076c2 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -306,7 +306,7 @@ pub(super) fn update( // private / public switch ui.add(Label::new("Private").selectable(false)); if ui - .add(widgets::Switch::onoff(&app.theme, &mut private)) + .add(widgets::Switch::small(&app.theme, &mut private)) .clicked() { let _ = GLOBALS.storage.add_person_to_list( @@ -424,8 +424,13 @@ fn render_add_contact_popup( ui.label("Search for known contacts to add"); ui.add_space(8.0); - let mut output = - widgets::search_field(ui, &mut app.people_list.add_contact_search, f32::INFINITY); + let mut output = widgets::TextEdit::search( + &app.theme, + &app.assets, + &mut app.people_list.add_contact_search, + ) + .desired_width(f32::INFINITY) + .show(ui); let mut selected = app.people_list.add_contact_search_selected; widgets::show_contact_search( @@ -611,7 +616,7 @@ pub(super) fn render_create_list_dialog(ui: &mut Ui, app: &mut GossipUi) { } ui.add_space(10.0); ui.horizontal(|ui| { - ui.add(widgets::Switch::onoff( + ui.add(widgets::Switch::small( &app.theme, &mut app.new_list_favorite, )); diff --git a/gossip-bin/src/ui/people/person.rs b/gossip-bin/src/ui/people/person.rs index f1827132..0dd34739 100644 --- a/gossip-bin/src/ui/people/person.rs +++ b/gossip-bin/src/ui/people/person.rs @@ -203,7 +203,7 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe let mut inlist = membership.is_some(); if ui - .add(widgets::Switch::onoff(&app.theme, &mut inlist)) + .add(widgets::Switch::small(&app.theme, &mut inlist)) .clicked() { if !inlist { @@ -228,7 +228,7 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe let mut private = !membership.unwrap_or(&false); let switch_response = - ui.add(widgets::Switch::onoff(&app.theme, &mut private)); + ui.add(widgets::Switch::small(&app.theme, &mut private)); if switch_response.clicked() { let _ = GLOBALS .storage diff --git a/gossip-bin/src/ui/relays/active.rs b/gossip-bin/src/ui/relays/active.rs index 4a332d20..0fee6642 100644 --- a/gossip-bin/src/ui/relays/active.rs +++ b/gossip-bin/src/ui/relays/active.rs @@ -20,7 +20,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_field(ui, &mut app.relays.search, 200.0); + widgets::TextEdit::search(&app.theme, &app.assets, &mut app.relays.search) + .desired_width(200.0) + .show(ui); ui.add_space(200.0); // search_field somehow doesn't "take up" space if ui .button(RichText::new(Page::RelaysCoverage.name())) diff --git a/gossip-bin/src/ui/relays/known.rs b/gossip-bin/src/ui/relays/known.rs index 375dc264..027e9d3e 100644 --- a/gossip-bin/src/ui/relays/known.rs +++ b/gossip-bin/src/ui/relays/known.rs @@ -16,7 +16,9 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_field(ui, &mut app.relays.search, 200.0); + widgets::TextEdit::search(&app.theme, &app.assets, &mut app.relays.search) + .desired_width(200.0) + .show(ui); }); // TBD time how long this takes. We don't want expensive code in the UI diff --git a/gossip-bin/src/ui/relays/mine.rs b/gossip-bin/src/ui/relays/mine.rs index 208c6ed5..6b7db46a 100644 --- a/gossip-bin/src/ui/relays/mine.rs +++ b/gossip-bin/src/ui/relays/mine.rs @@ -17,7 +17,9 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_field(ui, &mut app.relays.search, 200.0); + widgets::TextEdit::search(&app.theme, &app.assets, &mut app.relays.search) + .desired_width(200.0) + .show(ui); ui.add_space(200.0); // search_field somehow doesn't "take up" space widgets::set_important_button_visuals(ui, app); if ui.button("Advertise Relay List") diff --git a/gossip-bin/src/ui/relays/mod.rs b/gossip-bin/src/ui/relays/mod.rs index fc5dee50..942ff3c6 100644 --- a/gossip-bin/src/ui/relays/mod.rs +++ b/gossip-bin/src/ui/relays/mod.rs @@ -473,17 +473,21 @@ pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) { .with_min_size(min_size) .with_hover_text("Configure List View".to_owned()) .show(ui, |ui, is_open| { - let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - ui.horizontal(|ui| { - if widgets::switch_with_size(ui, &mut app.relays.show_details, size).changed() { + if widgets::Switch::small(&app.theme, &mut app.relays.show_details) + .show(ui) + .changed() + { *is_open = false; } ui.label("Show details"); }); ui.add_space(8.0); ui.horizontal(|ui| { - if widgets::switch_with_size(ui, &mut app.relays.show_hidden, size).changed() { + if widgets::Switch::small(&app.theme, &mut app.relays.show_hidden) + .show(ui) + .changed() + { *is_open = false; } ui.label("Show hidden relays"); diff --git a/gossip-bin/src/ui/theme/mod.rs b/gossip-bin/src/ui/theme/mod.rs index 03596d32..bf95338c 100644 --- a/gossip-bin/src/ui/theme/mod.rs +++ b/gossip-bin/src/ui/theme/mod.rs @@ -11,6 +11,8 @@ use std::collections::BTreeMap; mod default; pub use default::DefaultTheme; +pub(super) mod test_page; + pub fn apply_theme(theme: &Theme, ctx: &Context) { ctx.set_style(theme.get_style()); ctx.set_fonts(theme.font_definitions()); diff --git a/gossip-bin/src/ui/theme/test_page.rs b/gossip-bin/src/ui/theme/test_page.rs new file mode 100644 index 00000000..e57c2d58 --- /dev/null +++ b/gossip-bin/src/ui/theme/test_page.rs @@ -0,0 +1,441 @@ +use crate::ui::feed::NoteRenderData; +use crate::ui::widgets; +use crate::ui::GossipUi; +use crate::ui::HighlightType; +use eframe::egui; +use egui::text::LayoutJob; +use egui::widget_text::WidgetText; +use egui::{Color32, Context, Frame, Margin, RichText, Ui}; +use egui_winit::egui::Vec2; +use egui_winit::egui::Widget; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Background { + None, + Input, + Note, + HighlightedNote, +} + +pub struct ThemeTest { + textedit_empty: String, + textedit_filled: String, + switch_value: bool, +} + +impl Default for ThemeTest { + fn default() -> Self { + Self { + textedit_empty: Default::default(), + textedit_filled: "Some text".into(), + switch_value: false, + } + } +} + +pub(in crate::ui) fn update( + app: &mut GossipUi, + _ctx: &Context, + _frame: &mut eframe::Frame, + ui: &mut Ui, +) { + widgets::page_header(ui, "Theme Test", |_ui| {}); + + app.vert_scroll_area() + .id_source(ui.auto_id_with("theme_test")) + .show(ui, |ui| { + button_test(app, ui); + + ui.add_space(20.0); + + textedit_test(app, ui); + + ui.add_space(20.0); + + switch_test(app, ui); + + ui.add_space(20.0); + + align_test(app, ui); + + ui.add_space(20.0); + + // On No Background + Frame::none() + .inner_margin(Margin::symmetric(20.0, 20.0)) + .show(ui, |ui| { + ui.heading("No Background"); + inner(app, ui, Background::None); + }); + + // On Note Background + let render_data = NoteRenderData { + height: 200.0, + is_new: false, + is_main_event: false, + has_repost: false, + is_comment_mention: false, + is_thread: false, + is_first: true, + is_last: true, + thread_position: 0, + }; + Frame::none() + .inner_margin(app.theme.feed_frame_inner_margin(&render_data)) + .outer_margin(app.theme.feed_frame_outer_margin(&render_data)) + .rounding(app.theme.feed_frame_rounding(&render_data)) + .shadow(app.theme.feed_frame_shadow(&render_data)) + .fill(app.theme.feed_frame_fill(&render_data)) + .stroke(app.theme.feed_frame_stroke(&render_data)) + .show(ui, |ui| { + ui.heading("Note Background"); + ui.label("(with note margins)"); + inner(app, ui, Background::Note); + }); + + // On Highlighted Note Background + let render_data = NoteRenderData { + height: 200.0, + is_new: true, + is_main_event: false, + has_repost: false, + is_comment_mention: false, + is_thread: false, + is_first: true, + is_last: true, + thread_position: 0, + }; + Frame::none() + .inner_margin(app.theme.feed_frame_inner_margin(&render_data)) + .outer_margin(app.theme.feed_frame_outer_margin(&render_data)) + .rounding(app.theme.feed_frame_rounding(&render_data)) + .shadow(app.theme.feed_frame_shadow(&render_data)) + .fill(app.theme.feed_frame_fill(&render_data)) + .stroke(app.theme.feed_frame_stroke(&render_data)) + .show(ui, |ui| { + ui.heading("Unread Note Background"); + ui.label("(with note margins)"); + inner(app, ui, Background::HighlightedNote); + }); + + // On Input Background + Frame::none() + .fill(app.theme.get_style().visuals.extreme_bg_color) + .inner_margin(Margin::symmetric(20.0, 20.0)) + .show(ui, |ui| { + ui.heading("Input Background"); + inner(app, ui, Background::Input); + }); + }); +} + +fn inner(app: &mut GossipUi, ui: &mut Ui, background: Background) { + let theme = app.theme; + let accent = RichText::new("accent").color(theme.accent_color()); + let accent_complementary = RichText::new("accent complimentary (indirectly used)") + .color(theme.accent_complementary_color()); + + line(ui, accent); + line(ui, accent_complementary); + + if background == Background::Input { + for (ht, txt) in [ + (HighlightType::Nothing, "nothing"), + (HighlightType::PublicKey, "public key"), + (HighlightType::Event, "event"), + (HighlightType::Relay, "relay"), + (HighlightType::Hyperlink, "hyperlink"), + ] { + let mut highlight_job = LayoutJob::default(); + highlight_job.append( + &format!("highlight text format for {}", txt), + 0.0, + theme.highlight_text_format(ht), + ); + line(ui, WidgetText::LayoutJob(highlight_job)); + } + } + + if background == Background::Note || background == Background::HighlightedNote { + let warning_marker = + RichText::new("warning marker").color(theme.warning_marker_text_color()); + line(ui, warning_marker); + + let notice_marker = RichText::new("notice marker").color(theme.notice_marker_text_color()); + line(ui, notice_marker); + } + + if background != Background::Input { + ui.horizontal(|ui| { + ui.label(RichText::new("•").color(Color32::from_gray(128))); + crate::ui::widgets::break_anywhere_hyperlink_to( + ui, + "https://hyperlink.example.com", + "https://hyperlink.example.com", + ); + }); + } +} + +fn line(ui: &mut Ui, label: impl Into) { + let bullet = RichText::new("•").color(Color32::from_gray(128)); + ui.horizontal(|ui| { + ui.label(bullet); + ui.label(label); + }); +} + +fn button_test(app: &mut GossipUi, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.heading("Button Test:"); + ui.add_space(30.0); + }); + ui.add_space(30.0); + const TEXT: &str = "Continue"; + let theme = &app.theme; + const CSIZE: Vec2 = Vec2 { x: 100.0, y: 20.0 }; + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Default")); + ui.add_space(20.0); + widgets::Button::primary(theme, TEXT).draw_default(ui); + ui.add_space(20.0); + widgets::Button::secondary(theme, TEXT).draw_default(ui); + ui.add_space(20.0); + widgets::Button::bordered(theme, TEXT).draw_default(ui); + }); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Hovered")); + ui.add_space(20.0); + widgets::Button::primary(theme, TEXT).draw_hovered(ui); + ui.add_space(20.0); + widgets::Button::secondary(theme, TEXT).draw_hovered(ui); + ui.add_space(20.0); + widgets::Button::bordered(theme, TEXT).draw_hovered(ui); + }); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Active")); + ui.add_space(20.0); + widgets::Button::primary(theme, TEXT).draw_active(ui); + ui.add_space(20.0); + widgets::Button::secondary(theme, TEXT).draw_active(ui); + ui.add_space(20.0); + widgets::Button::bordered(theme, TEXT).draw_active(ui); + }); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Disabled")); + ui.add_space(20.0); + widgets::Button::primary(theme, TEXT).draw_disabled(ui); + ui.add_space(20.0); + widgets::Button::secondary(theme, TEXT).draw_disabled(ui); + ui.add_space(20.0); + widgets::Button::bordered(theme, TEXT).draw_disabled(ui); + }); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Focused")); + ui.add_space(20.0); + widgets::Button::primary(theme, TEXT).draw_focused(ui); + ui.add_space(20.0); + widgets::Button::secondary(theme, TEXT).draw_focused(ui); + ui.add_space(20.0); + widgets::Button::bordered(theme, TEXT).draw_focused(ui); + }); + ui.add_space(30.0); + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.add_sized(CSIZE, egui::Label::new("try it->")); + }); + ui.add_space(20.0); + ui.vertical(|ui| { + let response = widgets::Button::primary(theme, TEXT).ui(ui); + if ui.link("focus").clicked() { + response.request_focus(); + } + }); + ui.add_space(20.0); + ui.vertical(|ui| { + let response = widgets::Button::secondary(theme, TEXT).ui(ui); + if ui.link("focus").clicked() { + response.request_focus(); + } + }); + ui.add_space(20.0); + ui.vertical(|ui| { + let response = widgets::Button::bordered(theme, TEXT).ui(ui); + if ui.link("focus").clicked() { + response.request_focus(); + } + }); + }); + }); +} + +fn textedit_test(app: &mut GossipUi, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.heading("Button Test:"); + ui.add_space(30.0); + }); + ui.add_space(30.0); + let theme = &app.theme; + let assets = &app.assets; + const HINT: &str = "Placeholder"; + const CSIZE: Vec2 = Vec2 { x: 100.0, y: 20.0 }; + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Empty")); + ui.add_space(20.0); + ui.vertical(|ui| { + let output = + widgets::TextEdit::singleline(theme, &mut app.theme_test.textedit_empty) + .hint_text(HINT) + .show(ui); + if ui.link("focus").clicked() { + output.response.request_focus(); + } + }); + ui.add_space(20.0); + ui.vertical(|ui| { + let output = + widgets::TextEdit::search(theme, assets, &mut app.theme_test.textedit_empty) + .hint_text(HINT) + .show(ui); + if ui.link("focus").clicked() { + output.response.request_focus(); + } + }); + }); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("with Text")); + ui.add_space(20.0); + ui.vertical(|ui| { + let output = + widgets::TextEdit::singleline(theme, &mut app.theme_test.textedit_filled) + .hint_text(HINT) + .show(ui); + if ui.link("focus").clicked() { + output.response.request_focus(); + } + }); + ui.add_space(20.0); + ui.vertical(|ui| { + let output = + widgets::TextEdit::search(theme, assets, &mut app.theme_test.textedit_filled) + .hint_text(HINT) + .show(ui); + if ui.link("focus").clicked() { + output.response.request_focus(); + } + }); + }); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Disabled")); + ui.set_enabled(false); + ui.add_space(20.0); + ui.vertical(|ui| { + widgets::TextEdit::singleline(theme, &mut app.theme_test.textedit_empty) + .hint_text(HINT) + .show(ui); + }); + ui.add_space(20.0); + ui.vertical(|ui| { + widgets::TextEdit::search(theme, assets, &mut app.theme_test.textedit_empty) + .hint_text(HINT) + .show(ui); + }); + }); + }); +} + +fn switch_test(app: &mut GossipUi, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.heading("Switch Test:"); + ui.add_space(30.0); + }); + ui.add_space(30.0); + let theme = &app.theme; + const TEXT: &str = "Some text"; + const CSIZE: Vec2 = Vec2 { x: 100.0, y: 20.0 }; + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Enabled")); + ui.add_space(20.0); + ui.vertical(|ui| { + let response = ui + .horizontal(|ui| { + let response = ui.add( + widgets::Switch::small(theme, &mut app.theme_test.switch_value) + .with_label(TEXT), + ); + response + }) + .inner; + if ui.link("focus").clicked() { + response.request_focus(); + } + }); + ui.add_space(20.0); + ui.vertical(|ui| { + let response = ui + .horizontal(|ui| { + let response = ui.add( + widgets::Switch::large(theme, &mut app.theme_test.switch_value) + .with_label(TEXT), + ); + response + }) + .inner; + if ui.link("focus").clicked() { + response.request_focus(); + } + }); + }); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_sized(CSIZE, egui::Label::new("Disabled")); + ui.set_enabled(false); + ui.add_space(20.0); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.add(widgets::Switch::small(theme, &mut false).with_label(TEXT)); + }); + }); + ui.add_space(20.0); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.add(widgets::Switch::large(theme, &mut false).with_label(TEXT)); + }); + }); + }); + }); +} + +fn align_test(app: &mut GossipUi, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.heading("Horizontal Alignment Test:"); + ui.add_space(30.0); + }); + ui.add_space(30.0); + let theme = &app.theme; + ui.horizontal(|ui| { + ui.label("Text"); + widgets::Button::primary(theme, "Primary").show(ui); + ui.label("text"); + widgets::TextEdit::singleline(theme, &mut app.theme_test.textedit_filled) + .desired_width(100.0) + .show(ui); + ui.label("text"); + widgets::Switch::small(theme, &mut app.theme_test.switch_value) + .with_label("Switch") + .show(ui); + egui::ComboBox::from_label("Select").show_ui(ui, |ui| { + let _ = ui.selectable_label(false, "first"); + let _ = ui.selectable_label(false, "second"); + }); + }); +} diff --git a/gossip-bin/src/ui/widgets/button.rs b/gossip-bin/src/ui/widgets/button.rs index 89e2895b..851bbe8e 100644 --- a/gossip-bin/src/ui/widgets/button.rs +++ b/gossip-bin/src/ui/widgets/button.rs @@ -1,16 +1,30 @@ -use egui_winit::egui::{self, Response, Ui, Widget, WidgetText}; +use std::sync::Arc; -use super::super::Theme; +use egui_winit::egui::{ + self, vec2, Galley, NumExt, Rect, Response, Rounding, Sense, Stroke, TextStyle, Ui, Vec2, + Widget, WidgetInfo, WidgetText, WidgetType, +}; +use super::{super::Theme, WidgetState}; + +#[derive(Clone, Copy)] enum ButtonType { Primary, Secondary, + Bordered, +} + +enum ButtonVariant { + Normal, + // Small, + // Wide, } /// Clickable button with text #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct Button<'a> { button_type: ButtonType, + variant: ButtonVariant, theme: &'a Theme, text: Option, } @@ -19,6 +33,7 @@ impl<'a> Button<'a> { pub fn primary(theme: &'a Theme, text: impl Into) -> Self { Self { button_type: ButtonType::Primary, + variant: ButtonVariant::Normal, theme, text: Some(text.into()), } @@ -27,19 +42,423 @@ impl<'a> Button<'a> { pub fn secondary(theme: &'a Theme, text: impl Into) -> Self { Self { button_type: ButtonType::Secondary, + variant: ButtonVariant::Normal, theme, text: Some(text.into()), } } + + pub fn bordered(theme: &'a Theme, text: impl Into) -> Self { + Self { + button_type: ButtonType::Bordered, + variant: ButtonVariant::Normal, + theme, + text: Some(text.into()), + } + } + + // /// Make this a small button, suitable for embedding into text. + // pub fn small(mut self, small: bool) -> Self { + // if small { + // self.variant = ButtonVariant::Small; + // } + // self + // } + + // /// Make this a wide button. + // pub fn wide(mut self, wide: bool) -> Self { + // if wide { + // self.variant = ButtonVariant::Wide; + // } + // self + // } + + pub fn draw_default(self, ui: &mut Ui) -> Response { + let (text, desired_size, padding) = Self::layout(ui, self.text, self.variant); + let (rect, response) = Self::allocate(ui, &text, desired_size); + Self::draw( + ui, + text, + rect, + WidgetState::Default, + self.button_type, + padding, + self.theme, + ); + response + } + + pub fn draw_hovered(self, ui: &mut Ui) -> Response { + let (text, desired_size, padding) = Self::layout(ui, self.text, self.variant); + let (rect, response) = Self::allocate(ui, &text, desired_size); + Self::draw( + ui, + text, + rect, + WidgetState::Hovered, + self.button_type, + padding, + self.theme, + ); + response + } + + pub fn draw_active(self, ui: &mut Ui) -> Response { + let (text, desired_size, padding) = Self::layout(ui, self.text, self.variant); + let (rect, response) = Self::allocate(ui, &text, desired_size); + Self::draw( + ui, + text, + rect, + WidgetState::Active, + self.button_type, + padding, + self.theme, + ); + response + } + + pub fn draw_disabled(self, ui: &mut Ui) -> Response { + let (text, desired_size, padding) = Self::layout(ui, self.text, self.variant); + let (rect, response) = Self::allocate(ui, &text, desired_size); + Self::draw( + ui, + text, + rect, + WidgetState::Disabled, + self.button_type, + padding, + self.theme, + ); + response + } + + pub fn draw_focused(self, ui: &mut Ui) -> Response { + let (text, desired_size, padding) = Self::layout(ui, self.text, self.variant); + let (rect, response) = Self::allocate(ui, &text, desired_size); + Self::draw( + ui, + text, + rect, + WidgetState::Focused, + self.button_type, + padding, + self.theme, + ); + response + } + + pub fn show(self, ui: &mut Ui) -> Response { + let (text, desired_size, padding) = Self::layout(ui, self.text, self.variant); + let (rect, response) = Self::allocate(ui, &text, desired_size); + let state = if response.is_pointer_button_down_on() { + WidgetState::Active + } else if response.has_focus() { + WidgetState::Focused + } else if response.hovered() || response.highlighted() { + WidgetState::Hovered + } else if !ui.is_enabled() { + WidgetState::Disabled + } else { + WidgetState::Default + }; + Self::draw(ui, text, rect, state, self.button_type, padding, self.theme); + response + } } impl Widget for Button<'_> { fn ui(self, ui: &mut Ui) -> Response { - match self.button_type { - ButtonType::Primary => self.theme.primary_button_style(ui.style_mut()), - ButtonType::Secondary => self.theme.secondary_button_style(ui.style_mut()), - } - let button = egui::Button::opt_image_and_text(None, self.text); - button.ui(ui) + self.show(ui) + } +} + +impl Button<'_> { + fn layout( + ui: &mut Ui, + text: Option, + variant: ButtonVariant, + ) -> (Option>, Vec2, Vec2) { + let frame = ui.visuals().button_frame; + + let button_padding = if frame { + Vec2::new(14.0, 5.0) + } else { + Vec2::ZERO + }; + + // match variant { + // ButtonVariant::Normal => {} + // ButtonVariant::Small => { + // button_padding.y = 0.0; + // } + // ButtonVariant::Wide => { + // button_padding.x *= 3.0; + // } + // } + + let wrap = None; + let text_wrap_width = ui.available_width() - 2.0 * button_padding.x; + + let text = text.map(|text| text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button)); + + let mut desired_size = Vec2::ZERO; + if let Some(text) = &text { + desired_size.x += text.size().x; + desired_size.y = desired_size.y.max(text.size().y); + } + desired_size += 2.0 * button_padding; + match variant { + ButtonVariant::Normal => { + desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); + } // ButtonVariant::Wide | ButtonVariant::Small => {} + } + (text, desired_size, button_padding) + } + + fn allocate(ui: &mut Ui, text: &Option>, desired_size: Vec2) -> (Rect, Response) { + let (rect, response) = ui.allocate_at_least(desired_size, Sense::click()); + response.widget_info(|| { + if let Some(text) = text { + WidgetInfo::labeled(WidgetType::Button, text.text()) + } else { + WidgetInfo::new(WidgetType::Button) + } + }); + + if let Some(cursor) = ui.visuals().interact_cursor { + if response.hovered { + ui.ctx().set_cursor_icon(cursor); + } + } + + (rect, response) + } + + fn draw( + ui: &mut Ui, + text: Option>, + rect: Rect, + state: WidgetState, + button_type: ButtonType, + button_padding: Vec2, + theme: &Theme, + ) { + if ui.is_rect_visible(rect) { + let no_stroke = Stroke::NONE; + let neutral_300_stroke = Stroke::new(1.0, theme.neutral_300()); + let neutral_400_stroke = Stroke::new(1.0, theme.neutral_400()); + let neutral_500_stroke = Stroke::new(1.0, theme.neutral_500()); + let neutral_600_stroke = Stroke::new(1.0, theme.neutral_600()); + let (frame_fill, frame_stroke, text_color, under_stroke) = if ui.visuals().dark_mode { + match state { + WidgetState::Default => match button_type { + ButtonType::Primary => ( + theme.accent_dark(), + no_stroke, + theme.neutral_50(), + no_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_200(), + no_stroke, + theme.neutral_700(), + no_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_950(), + neutral_400_stroke, + theme.neutral_300(), + no_stroke, + ), + }, + WidgetState::Hovered => match button_type { + ButtonType::Primary => ( + theme.accent_dark_b20(), + no_stroke, + theme.neutral_50(), + no_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_50(), + no_stroke, + theme.accent_dark(), + no_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_950(), + neutral_300_stroke, + theme.neutral_200(), + no_stroke, + ), + }, + WidgetState::Active => match button_type { + ButtonType::Primary => ( + theme.accent_dark(), + no_stroke, + theme.neutral_50(), + no_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_200(), + no_stroke, + theme.neutral_700(), + no_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_950(), + neutral_400_stroke, + theme.neutral_300(), + no_stroke, + ), + }, + WidgetState::Disabled => ( + theme.neutral_700(), + no_stroke, + theme.neutral_500(), + no_stroke, + ), + WidgetState::Focused => match button_type { + ButtonType::Primary => ( + theme.accent_dark_b20(), + no_stroke, + theme.neutral_50(), + neutral_300_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_50(), + no_stroke, + theme.accent_dark(), + neutral_400_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_950(), + neutral_300_stroke, + theme.neutral_200(), + neutral_500_stroke, + ), + }, + } + } else { + match state { + WidgetState::Default => match button_type { + ButtonType::Primary => ( + theme.accent_light(), + no_stroke, + theme.neutral_50(), + no_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_700(), + no_stroke, + theme.neutral_100(), + no_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_100(), + neutral_500_stroke, + theme.neutral_800(), + no_stroke, + ), + }, + WidgetState::Hovered => match button_type { + ButtonType::Primary => ( + theme.accent_light_b20(), + no_stroke, + theme.neutral_50(), + no_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_900(), + no_stroke, + theme.neutral_100(), + no_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_50(), + neutral_600_stroke, + theme.neutral_800(), + no_stroke, + ), + }, + WidgetState::Active => match button_type { + ButtonType::Primary => ( + theme.accent_light(), + no_stroke, + theme.neutral_50(), + no_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_700(), + no_stroke, + theme.neutral_100(), + no_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_100(), + neutral_600_stroke, + theme.accent_light(), + no_stroke, + ), + }, + WidgetState::Disabled => ( + theme.neutral_300(), + no_stroke, + theme.neutral_400(), + no_stroke, + ), + WidgetState::Focused => match button_type { + ButtonType::Primary => ( + theme.accent_light_b20(), + no_stroke, + theme.neutral_50(), + neutral_300_stroke, + ), + ButtonType::Secondary => ( + theme.neutral_900(), + no_stroke, + theme.neutral_100(), + neutral_400_stroke, + ), + ButtonType::Bordered => ( + theme.neutral_50(), + neutral_600_stroke, + theme.neutral_800(), + neutral_400_stroke, + ), + }, + } + }; + + let shrink = Vec2::splat(frame_stroke.width / 2.0); + ui.painter().rect( + rect.shrink2(shrink), + Rounding::same(4.0), + frame_fill, + frame_stroke, + ); + + if let Some(galley) = text { + let text_pos = { + // Make sure button text is centered if within a centered layout + ui.layout() + .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) + .min + }; + let painter = ui.painter(); + painter.galley(text_pos, galley.clone(), text_color); + let text_rect = Rect::from_min_size(text_pos, galley.rect.size()); + let shapes = egui::Shape::dashed_line( + &[ + text_rect.left_bottom() + vec2(0.0, 0.0), + text_rect.right_bottom() + vec2(0.0, 0.0), + ], + under_stroke, + 3.0, + 3.0, + ); + painter.add(shapes); + } + } } } diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index b5304874..91981491 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -16,9 +16,8 @@ pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; mod nav_item; use eframe::egui::{FontId, Galley}; use egui_winit::egui::text::LayoutJob; -use egui_winit::egui::text_edit::TextEditOutput; use egui_winit::egui::{ - self, vec2, Align, FontSelection, Rect, Response, RichText, Rounding, Sense, Ui, WidgetText, + self, Align, FontSelection, Response, RichText, Rounding, Sense, Ui, WidgetText, }; pub use nav_item::NavItem; @@ -37,8 +36,8 @@ pub use information_popup::InformationPopup; pub use information_popup::ProfilePopup; mod switch; +pub use switch::switch_custom_at; pub use switch::Switch; -pub use switch::{switch_custom_at, switch_with_size}; mod textedit; pub use textedit::TextEdit; @@ -48,17 +47,13 @@ use super::{GossipUi, Theme}; pub const DROPDOWN_DISTANCE: f32 = 10.0; pub const TAGG_WIDTH: f32 = 200.0; -// pub fn break_anywhere_label(ui: &mut Ui, text: impl Into) { -// let mut job = text.into().into_text_job( -// ui.style(), -// FontSelection::Default, -// ui.layout().vertical_align(), -// ); -// job.job.sections.first_mut().unwrap().format.color = -// ui.visuals().widgets.noninteractive.fg_stroke.color; -// job.job.wrap.break_anywhere = true; -// ui.label(job.job); -// } +pub enum WidgetState { + Default, + Hovered, + Active, + Disabled, + Focused, +} pub fn page_header( ui: &mut Ui, @@ -167,36 +162,6 @@ pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into, url ui.hyperlink_to(job, url); } -pub fn search_field(ui: &mut Ui, field: &mut String, width: f32) -> TextEditOutput { - // search field - let output = TextEdit::singleline(field) - .text_color(ui.visuals().widgets.inactive.fg_stroke.color) - .desired_width(width) - .show(ui); - - let rect = Rect::from_min_size( - output.response.rect.right_top() - vec2(output.response.rect.height(), 0.0), - vec2(output.response.rect.height(), output.response.rect.height()), - ); - - // search clear button - if ui - .put( - rect, - NavItem::new("\u{2715}", field.is_empty()) - .color(ui.visuals().widgets.inactive.fg_stroke.color) - .active_color(ui.visuals().widgets.active.fg_stroke.color) - .hover_color(ui.visuals().hyperlink_color) - .sense(Sense::click()), - ) - .clicked() - { - field.clear(); - } - - output -} - pub(super) fn set_important_button_visuals(ui: &mut Ui, app: &GossipUi) { let visuals = ui.visuals_mut(); visuals.widgets.inactive.weak_bg_fill = app.theme.accent_color(); diff --git a/gossip-bin/src/ui/widgets/more_menu.rs b/gossip-bin/src/ui/widgets/more_menu.rs index 3f0379f3..ba82c89b 100644 --- a/gossip-bin/src/ui/widgets/more_menu.rs +++ b/gossip-bin/src/ui/widgets/more_menu.rs @@ -36,7 +36,7 @@ impl MoreMenu { above_or_below: None, hover_text: None, accent_color: app.theme.accent_color(), - options_symbol: app.options_symbol.clone(), + options_symbol: app.assets.options_symbol.clone(), style: MoreMenuStyle::Simple, } } @@ -52,7 +52,7 @@ impl MoreMenu { above_or_below: None, hover_text: None, accent_color: app.theme.accent_color(), - options_symbol: app.options_symbol.clone(), + options_symbol: app.assets.options_symbol.clone(), style: MoreMenuStyle::Bubble, } } diff --git a/gossip-bin/src/ui/widgets/relay_entry.rs b/gossip-bin/src/ui/widgets/relay_entry.rs index c97594f8..efa28242 100644 --- a/gossip-bin/src/ui/widgets/relay_entry.rs +++ b/gossip-bin/src/ui/widgets/relay_entry.rs @@ -211,7 +211,7 @@ impl RelayEntry { accent_hover, bg_fill: app.theme.main_content_bgcolor(), // highlight: None, - option_symbol: (&app.options_symbol).into(), + option_symbol: (&app.assets.options_symbol).into(), auth_require_permission: false, conn_require_permission: false, } diff --git a/gossip-bin/src/ui/widgets/switch.rs b/gossip-bin/src/ui/widgets/switch.rs index d199a493..b708533d 100644 --- a/gossip-bin/src/ui/widgets/switch.rs +++ b/gossip-bin/src/ui/widgets/switch.rs @@ -1,72 +1,100 @@ -use egui_winit::egui::{self, vec2, Color32, Id, Rect, Response, Stroke, Ui, Vec2, Widget}; +use std::{ops::Sub, sync::Arc}; + +use egui_winit::egui::{ + self, vec2, Color32, Galley, Id, Rect, Response, Stroke, TextStyle, Ui, Vec2, Widget, + WidgetText, +}; use crate::ui::Theme; +use super::WidgetState; + pub struct Switch<'a> { value: &'a mut bool, + text: Option, size: Vec2, - knob_fill: Option, - on_fill: Option, - off_fill: Option, + theme: &'a Theme, } impl<'a> Switch<'a> { - #[allow(unused)] - pub fn onoff(theme: &Theme, value: &'a mut bool) -> Self { + /// Create a small switch, similar to normal line height + pub fn small(theme: &'a Theme, value: &'a mut bool) -> Self { Self { value, - size: theme.get_style().spacing.interact_size.y * vec2(1.6, 0.8), - knob_fill: Some(theme.get_style().visuals.extreme_bg_color), - on_fill: Some(theme.accent_color()), - off_fill: Some(theme.get_style().visuals.widgets.inactive.bg_fill), + text: None, + size: vec2(29.0, 16.0), + theme, } } - #[allow(unused)] - pub fn toggle(theme: &Theme, value: &'a mut bool) -> Self { + /// Create a large switch + pub fn large(theme: &'a Theme, value: &'a mut bool) -> Self { Self { value, - size: theme.get_style().spacing.interact_size.y * vec2(1.6, 0.8), - knob_fill: None, - on_fill: None, - off_fill: None, + text: None, + size: vec2(40.0, 22.0), + theme, } } + + /// Add a label that will be displayed to the right of the switch + pub fn with_label(mut self, text: impl Into) -> Self { + self.text = Some(text.into()); + self + } + + pub fn show(mut self, ui: &mut Ui) -> Response { + let (response, galley) = self.allocate(ui); + let (state, response) = interact(ui, response, self.value); + draw_at( + ui, self.theme, self.value, response, self.size, galley, state, + ) + } + + // pub fn show_at(mut self, ui: &mut Ui, id: Id, rect: Rect) -> Response { + // let response = self.interact_at(ui, id, rect); + // let (state, response) = interact(ui, response, self.value); + // draw_at(ui, self.value, response, state, self.theme) + // } + + fn allocate(&mut self, ui: &mut Ui) -> (Response, Option>) { + let (extra_width, galley) = if let Some(text) = self.text.take() { + let available_width = ui.available_width() - self.size.y - ui.spacing().item_spacing.y; + let galley = text.into_galley(ui, Some(false), available_width, TextStyle::Body); + ( + galley.rect.width() + ui.spacing().item_spacing.y, + Some(galley), + ) + } else { + (0.0, None) + }; + let sense = if ui.is_enabled() { + egui::Sense::click() + } else { + egui::Sense::hover() + }; + // allocate + let (_, response) = ui.allocate_exact_size(self.size + vec2(extra_width, 0.0), sense); + (response, galley) + } + + // fn interact_at(&mut self, ui: &mut Ui, id: Id, rect: Rect) -> Response { + // let sense = if ui.is_enabled() { + // egui::Sense::click() + // } else { + // egui::Sense::hover() + // }; + // // just interact + // ui.interact(rect, id, sense) + // } } impl<'a> Widget for Switch<'a> { fn ui(self, ui: &mut Ui) -> Response { - let (rect, _) = ui.allocate_exact_size(self.size, egui::Sense::hover()); - let id = ui.auto_id_with("sw"); - switch_custom_at( - ui, - ui.is_enabled(), - self.value, - rect, - id, - self.knob_fill, - self.on_fill, - self.off_fill, - ) + self.show(ui) } } -pub fn switch_with_size(ui: &mut Ui, on: &mut bool, size: egui::Vec2) -> Response { - let (rect, _) = ui.allocate_exact_size(size, egui::Sense::click()); - switch_with_size_at(ui, on, size, rect.left_top(), ui.auto_id_with("sw")) -} - -pub fn switch_with_size_at( - ui: &mut Ui, - value: &mut bool, - size: egui::Vec2, - pos: egui::Pos2, - id: Id, -) -> Response { - let rect = Rect::from_min_size(pos, size); - switch_custom_at(ui, ui.is_enabled(), value, rect, id, None, None, None) -} - #[allow(clippy::too_many_arguments)] pub fn switch_custom_at( ui: &mut Ui, @@ -134,3 +162,169 @@ pub fn switch_custom_at( response } + +fn interact(ui: &Ui, response: Response, value: &mut bool) -> (WidgetState, Response) { + let (state, mut response) = if response.is_pointer_button_down_on() { + (WidgetState::Active, response) + } else if response.has_focus() { + (WidgetState::Focused, response) + } else if response.hovered() || response.highlighted() { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + (WidgetState::Hovered, response) + } else if !ui.is_enabled() { + (WidgetState::Disabled, response) + } else { + (WidgetState::Default, response) + }; + + if response.clicked() { + *value = !*value; + response.mark_changed(); + response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *value, "")); + } + + (state, response) +} + +fn draw_at( + ui: &mut Ui, + theme: &Theme, + value: &bool, + response: Response, + size: Vec2, + galley: Option>, + _state: WidgetState, +) -> Response { + let rect = response.rect; + if ui.is_rect_visible(rect) { + let how_on = ui.ctx().animate_bool(response.id, *value); + + let radius = 0.5 * rect.height(); + let stroke_width = 0.5; + let (bg_fill, frame_stroke, knob_fill, knob_stroke, text_color) = if theme.dark_mode { + if ui.is_enabled() { + if *value { + ( + // on + theme.accent_dark(), + Stroke::new(stroke_width, theme.accent_dark()), + theme.neutral_50(), + Stroke::new(stroke_width, theme.neutral_300()), + theme.neutral_50(), + ) + } else { + ( + // off + theme.neutral_800(), + Stroke::new(stroke_width, theme.neutral_600()), + theme.neutral_100(), + Stroke::new(stroke_width, theme.neutral_700()), + theme.neutral_50(), + ) + } + } else { + ( + // disabled + theme.neutral_800(), + Stroke::new(stroke_width, theme.neutral_600()), + theme.neutral_700(), + Stroke::new(stroke_width, theme.neutral_600()), + theme.neutral_400(), + ) + } + } else { + if ui.is_enabled() { + if *value { + ( + // on + theme.accent_light(), + Stroke::new(stroke_width, theme.accent_light()), + theme.neutral_50(), + Stroke::new(stroke_width, theme.neutral_300()), + theme.neutral_900(), + ) + } else { + ( + // off + theme.neutral_200(), + Stroke::new(stroke_width, theme.neutral_400()), + theme.neutral_50(), + Stroke::new(stroke_width, theme.neutral_300()), + theme.neutral_900(), + ) + } + } else { + ( + // disabled + theme.neutral_300(), + Stroke::new(stroke_width, theme.neutral_400()), + theme.neutral_400(), + Stroke::new(stroke_width, theme.neutral_300()), + theme.neutral_400(), + ) + } + }; + + // switch + let switch_rect = Rect::from_min_size(rect.min, size); + ui.painter() + .rect(switch_rect, radius, bg_fill, frame_stroke); + let circle_x = egui::lerp( + (switch_rect.left() + radius)..=(switch_rect.right() - radius), + how_on, + ); + let center = egui::pos2(circle_x, switch_rect.center().y); + ui.painter() + .circle(center, radius.sub(1.0), knob_fill, knob_stroke); + + // label + if let Some(galley) = galley { + let text_pos = switch_rect.right_top() + + vec2( + ui.spacing().item_spacing.x, + (switch_rect.height() - galley.rect.height()) / 2.0, + ); + ui.painter() + .galley_with_override_text_color(text_pos, galley, text_color); + } + + if response.has_focus() { + // focus ring + // https://www.researchgate.net/publication/265893293_Approximation_of_a_cubic_bezier_curve_by_circular_arcs_and_vice_versa + // figure 4, formula 7 + const K: f32 = 0.551_915_05; // 0.5519150244935105707435627; + const PHI: f32 = std::f32::consts::PI / 4.0; // 1/8 of circle + const GROW: f32 = 3.0; // amount to increase radius over switch rounding5 + let mut rect = switch_rect.expand(GROW); + let rad = rect.height() / 2.0; + rect.set_width(rect.height()); + let center = rect.center(); + let p1 = Vec2 { + x: -rad * f32::cos(PHI), + y: rad * f32::sin(PHI), + }; + let p4 = Vec2 { + x: -rad * f32::cos(PHI), + y: -rad * f32::sin(PHI), + }; + let p2 = Vec2 { + x: p1.x - K * rad * f32::sin(PHI), + y: p1.y - K * rad * f32::cos(PHI), + }; + let p3 = Vec2 { + x: p4.x - K * rad * f32::sin(PHI), + y: p4.y + K * rad * f32::cos(PHI), + }; + let points = [center + p1, center + p2, center + p3, center + p4]; + let ring = egui::epaint::CubicBezierShape::from_points_stroke( + points, + false, + Color32::TRANSPARENT, + egui::Stroke::new(1.0, frame_stroke.color), + ); + ui.painter().add(ring); + } + } + + response +} diff --git a/gossip-bin/src/ui/widgets/textedit.rs b/gossip-bin/src/ui/widgets/textedit.rs index cf4bea5a..5802f29c 100644 --- a/gossip-bin/src/ui/widgets/textedit.rs +++ b/gossip-bin/src/ui/widgets/textedit.rs @@ -1,6 +1,17 @@ -use egui_winit::egui::{self, vec2, Color32, Rect, TextBuffer, Widget, WidgetText}; +use egui_winit::egui::{ + self, load::SizedTexture, vec2, Color32, Rect, Rounding, Sense, Stroke, TextBuffer, + TextureHandle, Widget, WidgetText, +}; + +use crate::ui::{ + assets::{self, Assets}, + Theme, +}; + +use super::NavItem; pub struct TextEdit<'t> { + theme: &'t Theme, text: &'t mut dyn TextBuffer, multiline: bool, desired_width: Option, @@ -10,11 +21,21 @@ pub struct TextEdit<'t> { text_color: Option, with_paste: bool, with_clear: bool, + with_search: bool, + magnifyingglass_symbol: Option, } +const MARGIN: egui::Margin = egui::Margin { + left: 8.0, + right: 8.0, + top: 4.5, + bottom: 4.5, +}; + impl<'t> TextEdit<'t> { - pub fn singleline(text: &'t mut dyn TextBuffer) -> Self { + pub fn singleline(theme: &'t Theme, text: &'t mut dyn TextBuffer) -> Self { Self { + theme, text, multiline: false, desired_width: None, @@ -24,6 +45,25 @@ impl<'t> TextEdit<'t> { text_color: None, with_paste: false, with_clear: false, + with_search: false, + magnifyingglass_symbol: None, + } + } + + pub fn search(theme: &'t Theme, assets: &Assets, text: &'t mut dyn TextBuffer) -> Self { + Self { + theme, + text, + multiline: false, + desired_width: None, + hint_text: WidgetText::default(), + password: false, + bg_color: None, + text_color: None, + with_paste: false, + with_clear: true, + with_search: true, + magnifyingglass_symbol: Some(assets.magnifyingglass_symbol.clone()), } } @@ -85,19 +125,26 @@ impl<'t> TextEdit<'t> { pub fn show(self, ui: &mut egui::Ui) -> egui::text_edit::TextEditOutput { ui.scope(|ui| { - if ui.visuals().dark_mode { - ui.visuals_mut().extreme_bg_color = - self.bg_color.unwrap_or(egui::Color32::from_gray(0x47)); - } else { - ui.visuals_mut().extreme_bg_color = self.bg_color.unwrap_or(Color32::WHITE); - } + self.set_visuals(ui); + + let pre_space = if self.with_search { 20.0 } else { 0.0 }; + let margin = egui::Margin { + left: MARGIN.left + pre_space, + right: MARGIN.right, + top: MARGIN.top, + bottom: MARGIN.bottom, + }; + + let where_to_put_background = ui.painter().add(egui::Shape::Noop); let mut inner = match self.multiline { false => egui::widgets::TextEdit::singleline(self.text), true => egui::widgets::TextEdit::multiline(self.text), } + .frame(false) .password(self.password) - .hint_text(self.hint_text.clone()); + .hint_text(self.hint_text.clone()) + .margin(margin); // set margin if let Some(width) = self.desired_width { inner = inner.desired_width(width); @@ -107,9 +154,88 @@ impl<'t> TextEdit<'t> { inner = inner.text_color(color); } - // show inner + // ---- show inner ---- let output = inner.show(ui); + // ---- draw frame ---- + { + let theme = self.theme; + let response = &output.response; + let frame_rect = response.rect; + + // this is how egui chooses the visual style: + #[allow(clippy::if_same_then_else)] + let (bg_color, frame_stroke) = if ui.visuals().dark_mode { + if !response.sense.interactive() { + (theme.neutral_800(), Stroke::new(1.0, theme.neutral_400())) + } else if response.is_pointer_button_down_on() || response.has_focus() { + (theme.neutral_800(), Stroke::new(1.0, theme.neutral_300())) + } else if response.hovered() || response.highlighted() { + (theme.neutral_800(), Stroke::new(1.0, theme.neutral_400())) + } else { + (theme.neutral_800(), Stroke::new(1.0, theme.neutral_400())) + } + } else { + if !response.sense.interactive() { + (theme.neutral_50(), Stroke::new(1.0, theme.neutral_400())) + } else if response.is_pointer_button_down_on() || response.has_focus() { + (theme.neutral_50(), Stroke::new(1.0, theme.neutral_500())) + } else if response.hovered() || response.highlighted() { + (theme.neutral_50(), Stroke::new(1.0, theme.neutral_400())) + } else { + (theme.neutral_50(), Stroke::new(1.0, theme.neutral_400())) + } + }; + + let rounding = Rounding::same(6.0); + + let shape = + egui::epaint::RectShape::new(frame_rect, rounding, bg_color, frame_stroke); + + ui.painter().set(where_to_put_background, shape); + } + + // ---- draw decorations ---- + if self.with_search { + if let Some(symbol) = self.magnifyingglass_symbol { + let rect = Rect::from_center_size( + output.response.rect.left_center() + + vec2((MARGIN.left + pre_space) / 2.0, 0.0), + symbol.size_vec2() / (assets::SVG_OVERSAMPLE + ui.ctx().zoom_factor()), + ); + egui::Image::from_texture(SizedTexture::new(symbol.id(), symbol.size_vec2())) + .fit_to_exact_size(rect.size()) + .tint(if self.theme.dark_mode { + self.theme.neutral_500() + } else { + self.theme.neutral_400() + }) + .paint_at(ui, rect); + } + } + + if self.with_clear && !self.text.as_str().is_empty() { + let rect = Rect::from_min_size( + output.response.rect.right_top() - vec2(output.response.rect.height(), 0.0), + vec2(output.response.rect.height(), output.response.rect.height()), + ); + + // clear button + if ui + .put( + rect, + NavItem::new("\u{2715}", self.text.as_str().is_empty()) + .color(ui.visuals().widgets.inactive.fg_stroke.color) + .active_color(ui.visuals().widgets.active.fg_stroke.color) + .hover_color(ui.visuals().hyperlink_color) + .sense(Sense::click()), + ) + .clicked() + { + self.text.clear(); + } + } + // paste button if self.with_paste { let action_size = vec2(45.0, output.response.rect.height()); @@ -143,6 +269,43 @@ impl<'t> TextEdit<'t> { impl<'t> Widget for TextEdit<'t> { fn ui(self, ui: &mut egui_winit::egui::Ui) -> egui_winit::egui::Response { - self.show(ui).response + let output = self.show(ui); + output.response + } +} + +impl TextEdit<'_> { + fn set_visuals(&self, ui: &mut egui::Ui) { + // this is how egui chooses the visual style: + // if !response.sense.interactive() { + // &self.noninteractive + // } else if response.is_pointer_button_down_on() || response.has_focus() { + // &self.active + // } else if response.hovered() || response.highlighted() { + // &self.hovered + // } else { + // &self.inactive + // } + let theme = self.theme; + let visuals = ui.visuals_mut(); + + // cursor (enabled) + visuals.text_cursor = Stroke::new(3.0, theme.accent_color()); + + if visuals.dark_mode { + // text color (enabled) + visuals.widgets.inactive.fg_stroke.color = theme.neutral_50(); + + // text selection + visuals.selection.bg_fill = theme.accent_color(); + visuals.selection.stroke = Stroke::new(1.0, Color32::WHITE); + } else { + // text color (enabled) + visuals.widgets.inactive.fg_stroke.color = theme.neutral_800(); + + // text selection + visuals.selection.bg_fill = theme.accent_color(); + visuals.selection.stroke = Stroke::new(1.0, Color32::WHITE); + } } }