New Widgets: Manually port important changes from 'feature/color-palette' branch

This commit is contained in:
Bu5hm4nn 2024-04-16 12:41:14 -06:00
parent 45180003de
commit 12075d4243
24 changed files with 1433 additions and 326 deletions

3
assets/magnifyingglass.svg Executable file
View File

@ -0,0 +1,3 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg">
<path fill="#FFFFFF" d="m4.50586 9.44238c.97852 0 1.88086-.3164 2.61914-.84375l2.77734 2.77737c.12886.1289.29886.1933.48046.1933.3809 0 .6504-.2929.6504-.6679 0-.1758-.0586-.3399-.1875-.4688l-2.75976-2.76561c.58008-.76172.92578-1.70508.92578-2.73047 0-2.47851-2.02734-4.505856-4.50586-4.505856-2.47266 0-4.50586 2.021486-4.50586 4.505856 0 2.47852 2.02734 4.50586 4.50586 4.50586zm0-.97265c-1.93359 0-3.533204-1.59961-3.533204-3.53321 0-1.93359 1.599614-3.5332 3.533204-3.5332s3.5332 1.59961 3.5332 3.5332c0 1.9336-1.59961 3.53321-3.5332 3.53321z"/>
</svg>

After

Width:  |  Height:  |  Size: 643 B

View File

@ -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,
}
}
}

View File

@ -104,8 +104,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, ui: &mut Ui) {
ui.add_space(10.0); ui.add_space(10.0);
ui.label(RichText::new("Include replies").size(11.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::small(&app.theme, &mut app.mainfeed_include_nonroot)
if widgets::switch_with_size(ui, &mut app.mainfeed_include_nonroot, size) .show(ui)
.clicked() .clicked()
{ {
app.set_page( 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.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(16.0); ui.add_space(16.0);
ui.label(RichText::new("Everything").size(11.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::small(&app.theme, &mut app.inbox_include_indirect)
if widgets::switch_with_size(ui, &mut app.inbox_include_indirect, size) .show(ui)
.clicked() .clicked()
{ {
app.set_page( app.set_page(

View File

@ -5,7 +5,6 @@ use gossip_lib::PersonList;
mod about; mod about;
mod stats; mod stats;
mod theme;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
if app.page == Page::HelpHelp { 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); stats::update(app, ctx, _frame, ui);
} else if app.page == Page::HelpAbout { } else if app.page == Page::HelpAbout {
about::update(app, ctx, _frame, ui); about::update(app, ctx, _frame, ui);
} else if app.page == Page::HelpTheme {
theme::update(app, ctx, _frame, ui);
} }
} }

View File

@ -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<WidgetText>) {
let bullet = RichText::new("").color(Color32::from_gray(128));
ui.horizontal(|ui| {
ui.label(bullet);
ui.label(label);
});
}

View File

@ -1,6 +1,6 @@
macro_rules! text_edit_line { macro_rules! text_edit_line {
($app:ident, $var:expr) => { ($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()) .text_color($app.theme.input_text_color())
}; };
} }
@ -33,6 +33,7 @@ macro_rules! write_setting {
}; };
} }
mod assets;
mod components; mod components;
mod dm_chat_list; mod dm_chat_list;
mod feed; mod feed;
@ -76,9 +77,9 @@ use std::hash::Hash;
use std::rc::Rc; use std::rc::Rc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use usvg::TreeParsing;
use zeroize::Zeroize; use zeroize::Zeroize;
use self::assets::Assets;
use self::feed::Notes; use self::feed::Notes;
use self::notifications::NotificationData; use self::notifications::NotificationData;
use self::widgets::NavItem; use self::widgets::NavItem;
@ -157,7 +158,8 @@ enum Page {
HelpHelp, HelpHelp,
HelpStats, HelpStats,
HelpAbout, HelpAbout,
HelpTheme, #[allow(unused)]
ThemeTest,
Wizard(WizardPage), Wizard(WizardPage),
} }
@ -204,7 +206,7 @@ impl Page {
Page::HelpHelp => (SubMenu::Help.as_str(), "Troubleshooting".into()), Page::HelpHelp => (SubMenu::Help.as_str(), "Troubleshooting".into()),
Page::HelpStats => (SubMenu::Help.as_str(), "Stats".into()), Page::HelpStats => (SubMenu::Help.as_str(), "Stats".into()),
Page::HelpAbout => (SubMenu::Help.as_str(), "About".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()), Page::Wizard(wp) => ("Wizard", wp.as_str().to_string()),
} }
} }
@ -433,10 +435,10 @@ struct GossipUi {
feeds: feed::Feeds, feeds: feed::Feeds,
// General Data // General Data
assets: Assets,
about: About, about: About,
icon: TextureHandle, icon: TextureHandle,
placeholder_avatar: TextureHandle, placeholder_avatar: TextureHandle,
options_symbol: TextureHandle,
unsaved_settings: UnsavedSettings, unsaved_settings: UnsavedSettings,
theme: Theme, theme: Theme,
avatars: HashMap<PublicKey, TextureHandle>, avatars: HashMap<PublicKey, TextureHandle>,
@ -508,6 +510,8 @@ struct GossipUi {
wizard_state: WizardState, wizard_state: WizardState,
theme_test: crate::ui::theme::test_page::ThemeTest,
// Cached DM Channels // Cached DM Channels
dm_channel_cache: Vec<DmChannelData>, dm_channel_cache: Vec<DmChannelData>,
dm_channel_next_refresh: Instant, 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::Relays, egui::Id::new(SubMenu::Relays.as_id_str()));
submenu_ids.insert(SubMenu::Help, egui::Id::new(SubMenu::Help.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 icon_texture_handle = {
let bytes = include_bytes!("../../../logo/gossip.png"); let bytes = include_bytes!("../../../logo/gossip.png");
let image = image::load_from_memory(bytes).unwrap(); 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), 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 let mainfeed_include_nonroot = cctx
.egui_ctx .egui_ctx
.data_mut(|d| d.get_persisted(egui::Id::new("mainfeed_include_nonroot"))) .data_mut(|d| d.get_persisted(egui::Id::new("mainfeed_include_nonroot")))
@ -681,10 +671,11 @@ impl GossipUi {
submenu_ids, submenu_ids,
settings_tab: SettingsTab::Id, settings_tab: SettingsTab::Id,
feeds: feed::Feeds::default(), feeds: feed::Feeds::default(),
// load Assets, but load again when DPI changes
assets,
about: About::new(), about: About::new(),
icon: icon_texture_handle, icon: icon_texture_handle,
placeholder_avatar: placeholder_avatar_texture_handle, placeholder_avatar: placeholder_avatar_texture_handle,
options_symbol,
unsaved_settings: UnsavedSettings::load(), unsaved_settings: UnsavedSettings::load(),
theme, theme,
avatars: HashMap::new(), avatars: HashMap::new(),
@ -731,6 +722,7 @@ impl GossipUi {
zap_state: ZapState::None, zap_state: ZapState::None,
note_being_zapped: None, note_being_zapped: None,
wizard_state, wizard_state,
theme_test: Default::default(),
dm_channel_cache: vec![], dm_channel_cache: vec![],
dm_channel_next_refresh: Instant::now(), dm_channel_next_refresh: Instant::now(),
dm_channel_error: None, dm_channel_error: None,
@ -762,21 +754,8 @@ impl GossipUi {
// 'original' refers to 'before the user changes it in settings' // 'original' refers to 'before the user changes it in settings'
self.original_dpi_value = self.override_dpi_value; self.original_dpi_value = self.override_dpi_value;
// load SVG's again when DPI changes // Reload Assets when DPI changes
self.options_symbol = { self.assets = Assets::init(ctx);
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)
};
// Set global pixels_per_point_times_100, used for image scaling. // Set global pixels_per_point_times_100, used for image scaling.
// this would warrant reloading images but the user experience isn't great as // this would warrant reloading images but the user experience isn't great as
@ -900,7 +879,7 @@ impl GossipUi {
Page::Settings => { Page::Settings => {
self.close_all_menus_except_feeds(ctx); 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); 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::HelpHelp, None, true);
self.add_menu_item_page(ui, Page::HelpStats, 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::HelpAbout, None, true);
self.add_menu_item_page(ui, Page::HelpTheme, None, true);
}); });
self.after_openable_menu(ui, &cstate); 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 // -- Status Area
ui.with_layout(Layout::bottom_up(Align::LEFT), |ui| { ui.with_layout(Layout::bottom_up(Align::LEFT), |ui| {
notifications::draw_icons(self, ui); notifications::draw_icons(self, ui);
@ -1520,9 +1506,10 @@ impl eframe::App for GossipUi {
| Page::RelaysKnownNetwork(_) => relays::update(self, ctx, frame, ui), | Page::RelaysKnownNetwork(_) => relays::update(self, ctx, frame, ui),
Page::Search => search::update(self, ctx, frame, ui), Page::Search => search::update(self, ctx, frame, ui),
Page::Settings => settings::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) help::update(self, ctx, frame, ui)
} }
Page::ThemeTest => theme::test_page::update(self, ctx, frame, ui),
Page::Wizard(_) => unreachable!(), Page::Wizard(_) => unreachable!(),
} }
}); });
@ -2166,7 +2153,7 @@ fn force_login(app: &mut GossipUi, ctx: &Context) {
ui.add_space(16.0); 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) .password(true)
.with_paste() .with_paste()
.desired_width( 400.0) .desired_width( 400.0)

View File

@ -108,7 +108,8 @@ impl<'a> Notification<'a> for AuthRequest {
}); });
ui.add_space(10.0); ui.add_space(10.0);
ui.label("Remember"); 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"); .on_hover_text("store permission permanently");
}); });
}); });

View File

@ -144,7 +144,8 @@ impl<'a> Notification<'a> for ConnRequest {
}); });
ui.add_space(10.0); ui.add_space(10.0);
ui.label("Remember"); 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"); .on_hover_text("store permission permanently");
}); });
}); });

View File

@ -55,7 +55,6 @@ pub trait Notification<'a> {
} }
type NotificationHandle = Rc<RefCell<dyn for<'handle> Notification<'handle>>>; type NotificationHandle = Rc<RefCell<dyn for<'handle> Notification<'handle>>>;
const SWITCH_SIZE: Vec2 = Vec2 { x: 40.0, y: 20.0 };
pub struct NotificationData { pub struct NotificationData {
active: Vec<NotificationHandle>, active: Vec<NotificationHandle>,

View File

@ -131,12 +131,9 @@ impl<'a> Notification<'a> for Nip46Request {
}); });
ui.add_space(10.0); ui.add_space(10.0);
ui.label("Remember"); ui.label("Remember");
widgets::switch_with_size( widgets::Switch::large(theme, &mut self.remember)
ui, .show(ui)
&mut self.remember, .on_hover_text("store permission permanently");
super::SWITCH_SIZE,
)
.on_hover_text("store permission permanently");
}); });
}); });
}); });

View File

@ -306,7 +306,7 @@ pub(super) fn update(
// private / public switch // private / public switch
ui.add(Label::new("Private").selectable(false)); ui.add(Label::new("Private").selectable(false));
if ui if ui
.add(widgets::Switch::onoff(&app.theme, &mut private)) .add(widgets::Switch::small(&app.theme, &mut private))
.clicked() .clicked()
{ {
let _ = GLOBALS.storage.add_person_to_list( 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.label("Search for known contacts to add");
ui.add_space(8.0); ui.add_space(8.0);
let mut output = let mut output = widgets::TextEdit::search(
widgets::search_field(ui, &mut app.people_list.add_contact_search, f32::INFINITY); &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; let mut selected = app.people_list.add_contact_search_selected;
widgets::show_contact_search( 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.add_space(10.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add(widgets::Switch::onoff( ui.add(widgets::Switch::small(
&app.theme, &app.theme,
&mut app.new_list_favorite, &mut app.new_list_favorite,
)); ));

View File

@ -203,7 +203,7 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe
let mut inlist = membership.is_some(); let mut inlist = membership.is_some();
if ui if ui
.add(widgets::Switch::onoff(&app.theme, &mut inlist)) .add(widgets::Switch::small(&app.theme, &mut inlist))
.clicked() .clicked()
{ {
if !inlist { 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 mut private = !membership.unwrap_or(&false);
let switch_response = 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() { if switch_response.clicked() {
let _ = GLOBALS let _ = GLOBALS
.storage .storage

View File

@ -20,7 +20,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
btn_h_space!(ui); btn_h_space!(ui);
super::relay_sort_combo(app, ui); super::relay_sort_combo(app, ui);
btn_h_space!(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 ui.add_space(200.0); // search_field somehow doesn't "take up" space
if ui if ui
.button(RichText::new(Page::RelaysCoverage.name())) .button(RichText::new(Page::RelaysCoverage.name()))

View File

@ -16,7 +16,9 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
btn_h_space!(ui); btn_h_space!(ui);
super::relay_sort_combo(app, ui); super::relay_sort_combo(app, ui);
btn_h_space!(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 // TBD time how long this takes. We don't want expensive code in the UI

View File

@ -17,7 +17,9 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
btn_h_space!(ui); btn_h_space!(ui);
super::relay_sort_combo(app, ui); super::relay_sort_combo(app, ui);
btn_h_space!(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 ui.add_space(200.0); // search_field somehow doesn't "take up" space
widgets::set_important_button_visuals(ui, app); widgets::set_important_button_visuals(ui, app);
if ui.button("Advertise Relay List") if ui.button("Advertise Relay List")

View File

@ -473,17 +473,21 @@ pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) {
.with_min_size(min_size) .with_min_size(min_size)
.with_hover_text("Configure List View".to_owned()) .with_hover_text("Configure List View".to_owned())
.show(ui, |ui, is_open| { .show(ui, |ui, is_open| {
let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8);
ui.horizontal(|ui| { 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; *is_open = false;
} }
ui.label("Show details"); ui.label("Show details");
}); });
ui.add_space(8.0); ui.add_space(8.0);
ui.horizontal(|ui| { 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; *is_open = false;
} }
ui.label("Show hidden relays"); ui.label("Show hidden relays");

View File

@ -11,6 +11,8 @@ use std::collections::BTreeMap;
mod default; mod default;
pub use default::DefaultTheme; pub use default::DefaultTheme;
pub(super) mod test_page;
pub fn apply_theme(theme: &Theme, ctx: &Context) { pub fn apply_theme(theme: &Theme, ctx: &Context) {
ctx.set_style(theme.get_style()); ctx.set_style(theme.get_style());
ctx.set_fonts(theme.font_definitions()); ctx.set_fonts(theme.font_definitions());

View File

@ -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<WidgetText>) {
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");
});
});
}

View File

@ -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 { enum ButtonType {
Primary, Primary,
Secondary, Secondary,
Bordered,
}
enum ButtonVariant {
Normal,
// Small,
// Wide,
} }
/// Clickable button with text /// Clickable button with text
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Button<'a> { pub struct Button<'a> {
button_type: ButtonType, button_type: ButtonType,
variant: ButtonVariant,
theme: &'a Theme, theme: &'a Theme,
text: Option<WidgetText>, text: Option<WidgetText>,
} }
@ -19,6 +33,7 @@ impl<'a> Button<'a> {
pub fn primary(theme: &'a Theme, text: impl Into<WidgetText>) -> Self { pub fn primary(theme: &'a Theme, text: impl Into<WidgetText>) -> Self {
Self { Self {
button_type: ButtonType::Primary, button_type: ButtonType::Primary,
variant: ButtonVariant::Normal,
theme, theme,
text: Some(text.into()), text: Some(text.into()),
} }
@ -27,19 +42,423 @@ impl<'a> Button<'a> {
pub fn secondary(theme: &'a Theme, text: impl Into<WidgetText>) -> Self { pub fn secondary(theme: &'a Theme, text: impl Into<WidgetText>) -> Self {
Self { Self {
button_type: ButtonType::Secondary, button_type: ButtonType::Secondary,
variant: ButtonVariant::Normal,
theme, theme,
text: Some(text.into()), text: Some(text.into()),
} }
} }
pub fn bordered(theme: &'a Theme, text: impl Into<WidgetText>) -> 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<'_> { impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
match self.button_type { self.show(ui)
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); impl Button<'_> {
button.ui(ui) fn layout(
ui: &mut Ui,
text: Option<WidgetText>,
variant: ButtonVariant,
) -> (Option<Arc<Galley>>, 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<Arc<Galley>>, 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<Arc<Galley>>,
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);
}
}
} }
} }

View File

@ -16,9 +16,8 @@ pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE};
mod nav_item; mod nav_item;
use eframe::egui::{FontId, Galley}; use eframe::egui::{FontId, Galley};
use egui_winit::egui::text::LayoutJob; use egui_winit::egui::text::LayoutJob;
use egui_winit::egui::text_edit::TextEditOutput;
use egui_winit::egui::{ 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; pub use nav_item::NavItem;
@ -37,8 +36,8 @@ pub use information_popup::InformationPopup;
pub use information_popup::ProfilePopup; pub use information_popup::ProfilePopup;
mod switch; mod switch;
pub use switch::switch_custom_at;
pub use switch::Switch; pub use switch::Switch;
pub use switch::{switch_custom_at, switch_with_size};
mod textedit; mod textedit;
pub use textedit::TextEdit; pub use textedit::TextEdit;
@ -48,17 +47,13 @@ use super::{GossipUi, Theme};
pub const DROPDOWN_DISTANCE: f32 = 10.0; pub const DROPDOWN_DISTANCE: f32 = 10.0;
pub const TAGG_WIDTH: f32 = 200.0; pub const TAGG_WIDTH: f32 = 200.0;
// pub fn break_anywhere_label(ui: &mut Ui, text: impl Into<WidgetText>) { pub enum WidgetState {
// let mut job = text.into().into_text_job( Default,
// ui.style(), Hovered,
// FontSelection::Default, Active,
// ui.layout().vertical_align(), Disabled,
// ); Focused,
// 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 fn page_header<R>( pub fn page_header<R>(
ui: &mut Ui, ui: &mut Ui,
@ -167,36 +162,6 @@ pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into<WidgetText>, url
ui.hyperlink_to(job, 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) { pub(super) fn set_important_button_visuals(ui: &mut Ui, app: &GossipUi) {
let visuals = ui.visuals_mut(); let visuals = ui.visuals_mut();
visuals.widgets.inactive.weak_bg_fill = app.theme.accent_color(); visuals.widgets.inactive.weak_bg_fill = app.theme.accent_color();

View File

@ -36,7 +36,7 @@ impl MoreMenu {
above_or_below: None, above_or_below: None,
hover_text: None, hover_text: None,
accent_color: app.theme.accent_color(), accent_color: app.theme.accent_color(),
options_symbol: app.options_symbol.clone(), options_symbol: app.assets.options_symbol.clone(),
style: MoreMenuStyle::Simple, style: MoreMenuStyle::Simple,
} }
} }
@ -52,7 +52,7 @@ impl MoreMenu {
above_or_below: None, above_or_below: None,
hover_text: None, hover_text: None,
accent_color: app.theme.accent_color(), accent_color: app.theme.accent_color(),
options_symbol: app.options_symbol.clone(), options_symbol: app.assets.options_symbol.clone(),
style: MoreMenuStyle::Bubble, style: MoreMenuStyle::Bubble,
} }
} }

View File

@ -211,7 +211,7 @@ impl RelayEntry {
accent_hover, accent_hover,
bg_fill: app.theme.main_content_bgcolor(), bg_fill: app.theme.main_content_bgcolor(),
// highlight: None, // highlight: None,
option_symbol: (&app.options_symbol).into(), option_symbol: (&app.assets.options_symbol).into(),
auth_require_permission: false, auth_require_permission: false,
conn_require_permission: false, conn_require_permission: false,
} }

View File

@ -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 crate::ui::Theme;
use super::WidgetState;
pub struct Switch<'a> { pub struct Switch<'a> {
value: &'a mut bool, value: &'a mut bool,
text: Option<WidgetText>,
size: Vec2, size: Vec2,
knob_fill: Option<Color32>, theme: &'a Theme,
on_fill: Option<Color32>,
off_fill: Option<Color32>,
} }
impl<'a> Switch<'a> { impl<'a> Switch<'a> {
#[allow(unused)] /// Create a small switch, similar to normal line height
pub fn onoff(theme: &Theme, value: &'a mut bool) -> Self { pub fn small(theme: &'a Theme, value: &'a mut bool) -> Self {
Self { Self {
value, value,
size: theme.get_style().spacing.interact_size.y * vec2(1.6, 0.8), text: None,
knob_fill: Some(theme.get_style().visuals.extreme_bg_color), size: vec2(29.0, 16.0),
on_fill: Some(theme.accent_color()), theme,
off_fill: Some(theme.get_style().visuals.widgets.inactive.bg_fill),
} }
} }
#[allow(unused)] /// Create a large switch
pub fn toggle(theme: &Theme, value: &'a mut bool) -> Self { pub fn large(theme: &'a Theme, value: &'a mut bool) -> Self {
Self { Self {
value, value,
size: theme.get_style().spacing.interact_size.y * vec2(1.6, 0.8), text: None,
knob_fill: None, size: vec2(40.0, 22.0),
on_fill: None, theme,
off_fill: None,
} }
} }
/// Add a label that will be displayed to the right of the switch
pub fn with_label(mut self, text: impl Into<WidgetText>) -> 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<Arc<Galley>>) {
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> { impl<'a> Widget for Switch<'a> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let (rect, _) = ui.allocate_exact_size(self.size, egui::Sense::hover()); self.show(ui)
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,
)
} }
} }
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)] #[allow(clippy::too_many_arguments)]
pub fn switch_custom_at( pub fn switch_custom_at(
ui: &mut Ui, ui: &mut Ui,
@ -134,3 +162,169 @@ pub fn switch_custom_at(
response 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<Arc<Galley>>,
_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
}

View File

@ -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> { pub struct TextEdit<'t> {
theme: &'t Theme,
text: &'t mut dyn TextBuffer, text: &'t mut dyn TextBuffer,
multiline: bool, multiline: bool,
desired_width: Option<f32>, desired_width: Option<f32>,
@ -10,11 +21,21 @@ pub struct TextEdit<'t> {
text_color: Option<Color32>, text_color: Option<Color32>,
with_paste: bool, with_paste: bool,
with_clear: bool, with_clear: bool,
with_search: bool,
magnifyingglass_symbol: Option<TextureHandle>,
} }
const MARGIN: egui::Margin = egui::Margin {
left: 8.0,
right: 8.0,
top: 4.5,
bottom: 4.5,
};
impl<'t> TextEdit<'t> { 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 { Self {
theme,
text, text,
multiline: false, multiline: false,
desired_width: None, desired_width: None,
@ -24,6 +45,25 @@ impl<'t> TextEdit<'t> {
text_color: None, text_color: None,
with_paste: false, with_paste: false,
with_clear: 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 { pub fn show(self, ui: &mut egui::Ui) -> egui::text_edit::TextEditOutput {
ui.scope(|ui| { ui.scope(|ui| {
if ui.visuals().dark_mode { self.set_visuals(ui);
ui.visuals_mut().extreme_bg_color =
self.bg_color.unwrap_or(egui::Color32::from_gray(0x47)); let pre_space = if self.with_search { 20.0 } else { 0.0 };
} else { let margin = egui::Margin {
ui.visuals_mut().extreme_bg_color = self.bg_color.unwrap_or(Color32::WHITE); 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 { let mut inner = match self.multiline {
false => egui::widgets::TextEdit::singleline(self.text), false => egui::widgets::TextEdit::singleline(self.text),
true => egui::widgets::TextEdit::multiline(self.text), true => egui::widgets::TextEdit::multiline(self.text),
} }
.frame(false)
.password(self.password) .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 { if let Some(width) = self.desired_width {
inner = inner.desired_width(width); inner = inner.desired_width(width);
@ -107,9 +154,88 @@ impl<'t> TextEdit<'t> {
inner = inner.text_color(color); inner = inner.text_color(color);
} }
// show inner // ---- show inner ----
let output = inner.show(ui); 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 // paste button
if self.with_paste { if self.with_paste {
let action_size = vec2(45.0, output.response.rect.height()); let action_size = vec2(45.0, output.response.rect.height());
@ -143,6 +269,43 @@ impl<'t> TextEdit<'t> {
impl<'t> Widget for TextEdit<'t> { impl<'t> Widget for TextEdit<'t> {
fn ui(self, ui: &mut egui_winit::egui::Ui) -> egui_winit::egui::Response { 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);
}
} }
} }