mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-28 16:00:58 +00:00
New Widgets: Manually port important changes from 'feature/color-palette' branch
This commit is contained in:
parent
45180003de
commit
12075d4243
3
assets/magnifyingglass.svg
Executable file
3
assets/magnifyingglass.svg
Executable 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 |
68
gossip-bin/src/ui/assets.rs
Normal file
68
gossip-bin/src/ui/assets.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
@ -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<PublicKey, TextureHandle>,
|
||||
@ -508,6 +510,8 @@ struct GossipUi {
|
||||
|
||||
wizard_state: WizardState,
|
||||
|
||||
theme_test: crate::ui::theme::test_page::ThemeTest,
|
||||
|
||||
// Cached DM Channels
|
||||
dm_channel_cache: Vec<DmChannelData>,
|
||||
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)
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
@ -55,7 +55,6 @@ pub trait Notification<'a> {
|
||||
}
|
||||
|
||||
type NotificationHandle = Rc<RefCell<dyn for<'handle> Notification<'handle>>>;
|
||||
const SWITCH_SIZE: Vec2 = Vec2 { x: 40.0, y: 20.0 };
|
||||
|
||||
pub struct NotificationData {
|
||||
active: Vec<NotificationHandle>,
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
));
|
||||
|
@ -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
|
||||
|
@ -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()))
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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");
|
||||
|
@ -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());
|
||||
|
441
gossip-bin/src/ui/theme/test_page.rs
Normal file
441
gossip-bin/src/ui/theme/test_page.rs
Normal 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");
|
||||
});
|
||||
});
|
||||
}
|
@ -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<WidgetText>,
|
||||
}
|
||||
@ -19,6 +33,7 @@ impl<'a> Button<'a> {
|
||||
pub fn primary(theme: &'a Theme, text: impl Into<WidgetText>) -> 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<WidgetText>) -> Self {
|
||||
Self {
|
||||
button_type: ButtonType::Secondary,
|
||||
variant: ButtonVariant::Normal,
|
||||
theme,
|
||||
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<'_> {
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<WidgetText>) {
|
||||
// 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<R>(
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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<WidgetText>,
|
||||
size: Vec2,
|
||||
knob_fill: Option<Color32>,
|
||||
on_fill: Option<Color32>,
|
||||
off_fill: Option<Color32>,
|
||||
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<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> {
|
||||
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<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
|
||||
}
|
||||
|
@ -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<f32>,
|
||||
@ -10,11 +21,21 @@ pub struct TextEdit<'t> {
|
||||
text_color: Option<Color32>,
|
||||
with_paste: 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> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user