mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-19 01:24:21 +01:00
chrome: collapsible side panel
This implements the initial logic that makes the side panel collapsible. Since we don't have a proper hamburger control, we do the same thing we do on iOS for now.
This commit is contained in:
@@ -19,6 +19,7 @@ use tracing::{error, info};
|
|||||||
|
|
||||||
pub enum AppAction {
|
pub enum AppAction {
|
||||||
Note(NoteAction),
|
Note(NoteAction),
|
||||||
|
ToggleChrome,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait App {
|
pub trait App {
|
||||||
|
|||||||
@@ -16,12 +16,22 @@ use notedeck_ui::{AnimationHelper, ProfilePic};
|
|||||||
static ICON_WIDTH: f32 = 40.0;
|
static ICON_WIDTH: f32 = 40.0;
|
||||||
pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
|
pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Chrome {
|
pub struct Chrome {
|
||||||
active: i32,
|
active: i32,
|
||||||
|
open: bool,
|
||||||
apps: Vec<NotedeckApp>,
|
apps: Vec<NotedeckApp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Chrome {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
active: 0,
|
||||||
|
open: true,
|
||||||
|
apps: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ChromePanelAction {
|
pub enum ChromePanelAction {
|
||||||
Support,
|
Support,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -85,6 +95,10 @@ impl Chrome {
|
|||||||
Chrome::default()
|
Chrome::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle(&mut self) {
|
||||||
|
self.open = !self.open;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_app(&mut self, app: NotedeckApp) {
|
pub fn add_app(&mut self, app: NotedeckApp) {
|
||||||
self.apps.push(app);
|
self.apps.push(app);
|
||||||
}
|
}
|
||||||
@@ -132,8 +146,11 @@ impl Chrome {
|
|||||||
let mut got_action: Option<ChromePanelAction> = None;
|
let mut got_action: Option<ChromePanelAction> = None;
|
||||||
let side_panel_width: f32 = 70.0;
|
let side_panel_width: f32 = 70.0;
|
||||||
|
|
||||||
|
let open_id = egui::Id::new("chrome_open");
|
||||||
|
let amt_open = ui.ctx().animate_bool(open_id, self.open) * side_panel_width;
|
||||||
|
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.size(Size::exact(side_panel_width)) // collapsible sidebar
|
.size(Size::exact(amt_open)) // collapsible sidebar
|
||||||
.size(Size::remainder()) // the main app contents
|
.size(Size::remainder()) // the main app contents
|
||||||
.clip(true)
|
.clip(true)
|
||||||
.horizontal(|mut strip| {
|
.horizontal(|mut strip| {
|
||||||
@@ -294,7 +311,7 @@ impl Chrome {
|
|||||||
|
|
||||||
if ui.add(expand_side_panel_button()).clicked() {
|
if ui.add(expand_side_panel_button()).clicked() {
|
||||||
//self.active = (self.active + 1) % (self.apps.len() as i32);
|
//self.active = (self.active + 1) % (self.apps.len() as i32);
|
||||||
// TODO: collapse sidebar ?
|
self.open = !self.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
@@ -492,6 +509,10 @@ fn chrome_handle_app_action(
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
) {
|
) {
|
||||||
match action {
|
match action {
|
||||||
|
AppAction::ToggleChrome => {
|
||||||
|
chrome.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
AppAction::Note(note_action) => {
|
AppAction::Note(note_action) => {
|
||||||
chrome.switch_to_columns();
|
chrome.switch_to_columns();
|
||||||
let Some(columns) = chrome.get_columns() else {
|
let Some(columns) = chrome.get_columns() else {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
column::Columns,
|
column::Columns,
|
||||||
decks::{Decks, DecksCache, FALLBACK_PUBKEY},
|
decks::{Decks, DecksCache, FALLBACK_PUBKEY},
|
||||||
draft::Drafts,
|
draft::Drafts,
|
||||||
nav,
|
nav::{self, ProcessNavResult},
|
||||||
route::Route,
|
route::Route,
|
||||||
storage,
|
storage,
|
||||||
subscriptions::{SubKind, Subscriptions},
|
subscriptions::{SubKind, Subscriptions},
|
||||||
@@ -340,15 +340,21 @@ fn process_message(damus: &mut Damus, ctx: &mut AppContext<'_>, relay: &str, msg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) {
|
fn render_damus(
|
||||||
if notedeck::ui::is_narrow(ui.ctx()) {
|
damus: &mut Damus,
|
||||||
render_damus_mobile(damus, app_ctx, ui);
|
app_ctx: &mut AppContext<'_>,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
) -> Option<AppAction> {
|
||||||
|
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
|
||||||
|
render_damus_mobile(damus, app_ctx, ui)
|
||||||
} else {
|
} else {
|
||||||
render_damus_desktop(damus, app_ctx, ui);
|
render_damus_desktop(damus, app_ctx, ui)
|
||||||
}
|
};
|
||||||
|
|
||||||
// We use this for keeping timestamps and things up to date
|
// We use this for keeping timestamps and things up to date
|
||||||
ui.ctx().request_repaint_after(Duration::from_secs(1));
|
ui.ctx().request_repaint_after(Duration::from_secs(1));
|
||||||
|
|
||||||
|
app_action
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -518,18 +524,33 @@ fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#[profiling::function]
|
#[profiling::function]
|
||||||
fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) {
|
fn render_damus_mobile(
|
||||||
|
app: &mut Damus,
|
||||||
|
app_ctx: &mut AppContext<'_>,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
) -> Option<AppAction> {
|
||||||
//let routes = app.timelines[0].routes.clone();
|
//let routes = app.timelines[0].routes.clone();
|
||||||
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
let mut app_action: Option<AppAction> = None;
|
||||||
|
|
||||||
if !app.columns(app_ctx.accounts).columns().is_empty()
|
if !app.columns(app_ctx.accounts).columns().is_empty() {
|
||||||
&& nav::render_nav(0, ui.available_rect_before_wrap(), app, app_ctx, ui)
|
let r = nav::render_nav(0, ui.available_rect_before_wrap(), app, app_ctx, ui)
|
||||||
.process_render_nav_response(app, app_ctx, ui)
|
.process_render_nav_response(app, app_ctx, ui);
|
||||||
&& !app.tmp_columns
|
if let Some(r) = &r {
|
||||||
{
|
match r {
|
||||||
|
ProcessNavResult::SwitchOccurred => {
|
||||||
|
if !app.tmp_columns {
|
||||||
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
|
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessNavResult::PfpClicked => {
|
||||||
|
app_action = Some(AppAction::ToggleChrome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rect.min.x = rect.max.x - 100.0;
|
rect.min.x = rect.max.x - 100.0;
|
||||||
rect.min.y = rect.max.y - 100.0;
|
rect.min.y = rect.max.y - 100.0;
|
||||||
@@ -549,10 +570,16 @@ fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut e
|
|||||||
router.route_to(Route::ComposeNote);
|
router.route_to(Route::ComposeNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app_action
|
||||||
}
|
}
|
||||||
|
|
||||||
#[profiling::function]
|
#[profiling::function]
|
||||||
fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) {
|
fn render_damus_desktop(
|
||||||
|
app: &mut Damus,
|
||||||
|
app_ctx: &mut AppContext<'_>,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
) -> Option<AppAction> {
|
||||||
let screen_size = ui.ctx().screen_rect().width();
|
let screen_size = ui.ctx().screen_rect().width();
|
||||||
let calc_panel_width = (screen_size
|
let calc_panel_width = (screen_size
|
||||||
/ get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32)
|
/ get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32)
|
||||||
@@ -566,16 +593,22 @@ fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut
|
|||||||
};
|
};
|
||||||
|
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
|
||||||
if need_scroll {
|
if need_scroll {
|
||||||
egui::ScrollArea::horizontal().show(ui, |ui| {
|
egui::ScrollArea::horizontal()
|
||||||
timelines_view(ui, panel_sizes, app, app_ctx);
|
.show(ui, |ui| timelines_view(ui, panel_sizes, app, app_ctx))
|
||||||
});
|
.inner
|
||||||
} else {
|
} else {
|
||||||
timelines_view(ui, panel_sizes, app, app_ctx);
|
timelines_view(ui, panel_sizes, app, app_ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut AppContext<'_>) {
|
fn timelines_view(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
sizes: Size,
|
||||||
|
app: &mut Damus,
|
||||||
|
ctx: &mut AppContext<'_>,
|
||||||
|
) -> Option<AppAction> {
|
||||||
let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns();
|
let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns();
|
||||||
let mut side_panel_action: Option<nav::SwitchingAction> = None;
|
let mut side_panel_action: Option<nav::SwitchingAction> = None;
|
||||||
let mut responses = Vec::with_capacity(num_cols);
|
let mut responses = Vec::with_capacity(num_cols);
|
||||||
@@ -654,9 +687,20 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App
|
|||||||
save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx);
|
save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut app_action: Option<AppAction> = None;
|
||||||
|
|
||||||
for response in responses {
|
for response in responses {
|
||||||
let save = response.process_render_nav_response(app, ctx, ui);
|
let nav_result = response.process_render_nav_response(app, ctx, ui);
|
||||||
save_cols = save_cols || save;
|
|
||||||
|
if let Some(nr) = &nav_result {
|
||||||
|
match nr {
|
||||||
|
ProcessNavResult::SwitchOccurred => save_cols = true,
|
||||||
|
|
||||||
|
ProcessNavResult::PfpClicked => {
|
||||||
|
app_action = Some(AppAction::ToggleChrome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.tmp_columns {
|
if app.tmp_columns {
|
||||||
@@ -666,6 +710,8 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App
|
|||||||
if save_cols {
|
if save_cols {
|
||||||
storage::save_decks_cache(ctx.path, &app.decks_cache);
|
storage::save_decks_cache(ctx.path, &app.decks_cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app_action
|
||||||
}
|
}
|
||||||
|
|
||||||
impl notedeck::App for Damus {
|
impl notedeck::App for Damus {
|
||||||
@@ -677,9 +723,7 @@ impl notedeck::App for Damus {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
update_damus(self, ctx, ui.ctx());
|
update_damus(self, ctx, ui.ctx());
|
||||||
render_damus(self, ctx, ui);
|
render_damus(self, ctx, ui)
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,24 @@ use notedeck::{
|
|||||||
use notedeck_ui::View;
|
use notedeck_ui::View;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
/// The result of processing a nav response
|
||||||
|
pub enum ProcessNavResult {
|
||||||
|
SwitchOccurred,
|
||||||
|
PfpClicked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessNavResult {
|
||||||
|
pub fn switch_occurred(&self) -> bool {
|
||||||
|
matches!(self, Self::SwitchOccurred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum RenderNavAction {
|
pub enum RenderNavAction {
|
||||||
Back,
|
Back,
|
||||||
RemoveColumn,
|
RemoveColumn,
|
||||||
|
/// The response when the user interacts with a pfp in the nav header
|
||||||
|
PfpClicked,
|
||||||
PostAction(NewPostAction),
|
PostAction(NewPostAction),
|
||||||
NoteAction(NoteAction),
|
NoteAction(NoteAction),
|
||||||
ProfileAction(ProfileAction),
|
ProfileAction(ProfileAction),
|
||||||
@@ -144,11 +158,10 @@ impl RenderNavResponse {
|
|||||||
app: &mut Damus,
|
app: &mut Damus,
|
||||||
ctx: &mut AppContext<'_>,
|
ctx: &mut AppContext<'_>,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
) -> bool {
|
) -> Option<ProcessNavResult> {
|
||||||
match self.response {
|
match self.response {
|
||||||
NotedeckNavResponse::Popup(nav_action) => {
|
NotedeckNavResponse::Popup(nav_action) => {
|
||||||
process_popup_resp(*nav_action, app, ctx, ui, self.column);
|
process_popup_resp(*nav_action, app, ctx, ui, self.column)
|
||||||
false
|
|
||||||
}
|
}
|
||||||
NotedeckNavResponse::Nav(nav_response) => {
|
NotedeckNavResponse::Nav(nav_response) => {
|
||||||
process_nav_resp(app, ctx, ui, *nav_response, self.column)
|
process_nav_resp(app, ctx, ui, *nav_response, self.column)
|
||||||
@@ -163,10 +176,10 @@ fn process_popup_resp(
|
|||||||
ctx: &mut AppContext<'_>,
|
ctx: &mut AppContext<'_>,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
col: usize,
|
col: usize,
|
||||||
) -> bool {
|
) -> Option<ProcessNavResult> {
|
||||||
let mut switching_occured = false;
|
let mut process_result: Option<ProcessNavResult> = None;
|
||||||
if let Some(nav_action) = action.response {
|
if let Some(nav_action) = action.response {
|
||||||
switching_occured = process_render_nav_action(app, ctx, ui, col, nav_action);
|
process_result = process_render_nav_action(app, ctx, ui, col, nav_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(NavAction::Returned) = action.action {
|
if let Some(NavAction::Returned) = action.action {
|
||||||
@@ -177,7 +190,7 @@ fn process_popup_resp(
|
|||||||
column.sheet_router.navigating = false;
|
column.sheet_router.navigating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
switching_occured
|
process_result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_nav_resp(
|
fn process_nav_resp(
|
||||||
@@ -186,13 +199,13 @@ fn process_nav_resp(
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
response: NavResponse<Option<RenderNavAction>>,
|
response: NavResponse<Option<RenderNavAction>>,
|
||||||
col: usize,
|
col: usize,
|
||||||
) -> bool {
|
) -> Option<ProcessNavResult> {
|
||||||
let mut switching_occured: bool = false;
|
let mut process_result: Option<ProcessNavResult> = None;
|
||||||
|
|
||||||
if let Some(action) = response.response.or(response.title_response) {
|
if let Some(action) = response.response.or(response.title_response) {
|
||||||
// start returning when we're finished posting
|
// start returning when we're finished posting
|
||||||
|
|
||||||
switching_occured = process_render_nav_action(app, ctx, ui, col, action);
|
process_result = process_render_nav_action(app, ctx, ui, col, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(action) = response.action {
|
if let Some(action) = response.action {
|
||||||
@@ -210,7 +223,7 @@ fn process_nav_resp(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
switching_occured = true;
|
process_result = Some(ProcessNavResult::SwitchOccurred);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavAction::Navigated => {
|
NavAction::Navigated => {
|
||||||
@@ -219,7 +232,8 @@ fn process_nav_resp(
|
|||||||
if cur_router.is_replacing() {
|
if cur_router.is_replacing() {
|
||||||
cur_router.remove_previous_routes();
|
cur_router.remove_previous_routes();
|
||||||
}
|
}
|
||||||
switching_occured = true;
|
|
||||||
|
process_result = Some(ProcessNavResult::SwitchOccurred);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavAction::Dragging => {}
|
NavAction::Dragging => {}
|
||||||
@@ -229,11 +243,15 @@ fn process_nav_resp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switching_occured
|
process_result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum RouterAction {
|
pub enum RouterAction {
|
||||||
GoBack,
|
GoBack,
|
||||||
|
/// We clicked on a pfp in a route. We currently don't carry any
|
||||||
|
/// information about the pfp since we only use it for toggling the
|
||||||
|
/// chrome atm
|
||||||
|
PfpClicked,
|
||||||
RouteTo(Route, RouterType),
|
RouteTo(Route, RouterType),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +265,7 @@ impl RouterAction {
|
|||||||
self,
|
self,
|
||||||
stack_router: &mut Router<Route>,
|
stack_router: &mut Router<Route>,
|
||||||
sheet_router: &mut SingletonRouter<Route>,
|
sheet_router: &mut SingletonRouter<Route>,
|
||||||
) {
|
) -> Option<ProcessNavResult> {
|
||||||
match self {
|
match self {
|
||||||
RouterAction::GoBack => {
|
RouterAction::GoBack => {
|
||||||
if sheet_router.route().is_some() {
|
if sheet_router.route().is_some() {
|
||||||
@@ -255,10 +273,21 @@ impl RouterAction {
|
|||||||
} else {
|
} else {
|
||||||
stack_router.go_back();
|
stack_router.go_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RouterAction::PfpClicked => Some(ProcessNavResult::PfpClicked),
|
||||||
|
|
||||||
RouterAction::RouteTo(route, router_type) => match router_type {
|
RouterAction::RouteTo(route, router_type) => match router_type {
|
||||||
RouterType::Sheet => sheet_router.route_to(route),
|
RouterType::Sheet => {
|
||||||
RouterType::Stack => stack_router.route_to(route),
|
sheet_router.route_to(route);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
RouterType::Stack => {
|
||||||
|
stack_router.route_to(route);
|
||||||
|
None
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,9 +307,10 @@ fn process_render_nav_action(
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
col: usize,
|
col: usize,
|
||||||
action: RenderNavAction,
|
action: RenderNavAction,
|
||||||
) -> bool {
|
) -> Option<ProcessNavResult> {
|
||||||
let router_action = match action {
|
let router_action = match action {
|
||||||
RenderNavAction::Back => Some(RouterAction::GoBack),
|
RenderNavAction::Back => Some(RouterAction::GoBack),
|
||||||
|
RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked),
|
||||||
|
|
||||||
RenderNavAction::RemoveColumn => {
|
RenderNavAction::RemoveColumn => {
|
||||||
let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
|
let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
|
||||||
@@ -291,7 +321,7 @@ fn process_render_nav_action(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return Some(ProcessNavResult::SwitchOccurred);
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderNavAction::PostAction(new_post_action) => {
|
RenderNavAction::PostAction(new_post_action) => {
|
||||||
@@ -326,7 +356,11 @@ fn process_render_nav_action(
|
|||||||
}
|
}
|
||||||
|
|
||||||
RenderNavAction::SwitchingAction(switching_action) => {
|
RenderNavAction::SwitchingAction(switching_action) => {
|
||||||
return switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx);
|
if switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx) {
|
||||||
|
return Some(ProcessNavResult::SwitchOccurred);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RenderNavAction::ProfileAction(profile_action) => profile_action.process(
|
RenderNavAction::ProfileAction(profile_action) => profile_action.process(
|
||||||
&mut app.view_state.pubkey_to_profile_state,
|
&mut app.view_state.pubkey_to_profile_state,
|
||||||
@@ -342,10 +376,11 @@ fn process_render_nav_action(
|
|||||||
let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col);
|
let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col);
|
||||||
let router = &mut cols.router;
|
let router = &mut cols.router;
|
||||||
let sheet_router = &mut cols.sheet_router;
|
let sheet_router = &mut cols.sheet_router;
|
||||||
action.process(router, sheet_router);
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
action.process(router, sheet_router)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_nav_body(
|
fn render_nav_body(
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ use crate::{
|
|||||||
ui::{self},
|
ui::{self},
|
||||||
};
|
};
|
||||||
|
|
||||||
use egui::Margin;
|
use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder};
|
||||||
use egui::{RichText, Stroke, UiBuilder};
|
|
||||||
use enostr::Pubkey;
|
use enostr::Pubkey;
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::{Images, NotedeckTextStyle};
|
use notedeck::{Images, NotedeckTextStyle};
|
||||||
@@ -84,8 +83,10 @@ impl<'a> NavTitle<'a> {
|
|||||||
let title_resp = self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some());
|
let title_resp = self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some());
|
||||||
|
|
||||||
if let Some(resp) = title_resp {
|
if let Some(resp) = title_resp {
|
||||||
|
tracing::debug!("got title response {resp:?}");
|
||||||
match resp {
|
match resp {
|
||||||
TitleResponse::RemoveColumn => Some(RenderNavAction::RemoveColumn),
|
TitleResponse::RemoveColumn => Some(RenderNavAction::RemoveColumn),
|
||||||
|
TitleResponse::PfpClicked => Some(RenderNavAction::PfpClicked),
|
||||||
TitleResponse::MoveColumn(to_index) => {
|
TitleResponse::MoveColumn(to_index) => {
|
||||||
let from = self.col_id;
|
let from = self.col_id;
|
||||||
Some(RenderNavAction::SwitchingAction(SwitchingAction::Columns(
|
Some(RenderNavAction::SwitchingAction(SwitchingAction::Columns(
|
||||||
@@ -94,6 +95,7 @@ impl<'a> NavTitle<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if back_button_resp.is_some_and(|r| r.clicked()) {
|
} else if back_button_resp.is_some_and(|r| r.clicked()) {
|
||||||
|
tracing::debug!("render nav action back");
|
||||||
Some(RenderNavAction::Back)
|
Some(RenderNavAction::Back)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -395,89 +397,95 @@ impl<'a> NavTitle<'a> {
|
|||||||
.get_profile_by_pubkey(txn, pubkey)
|
.get_profile_by_pubkey(txn, pubkey)
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(move |p| Some(ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size)))
|
.and_then(move |p| {
|
||||||
|
Some(
|
||||||
|
ProfilePic::from_profile(self.img_cache, p)?
|
||||||
|
.size(pfp_size)
|
||||||
|
.sense(Sense::click()),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) {
|
fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) -> Response {
|
||||||
let txn = Transaction::new(self.ndb).unwrap();
|
let txn = Transaction::new(self.ndb).unwrap();
|
||||||
|
|
||||||
if let Some(mut pfp) = id
|
if let Some(mut pfp) = id
|
||||||
.pubkey()
|
.pubkey()
|
||||||
.and_then(|pk| self.pubkey_pfp(&txn, pk.bytes(), pfp_size))
|
.and_then(|pk| self.pubkey_pfp(&txn, pk.bytes(), pfp_size))
|
||||||
{
|
{
|
||||||
ui.add(&mut pfp);
|
ui.add(&mut pfp)
|
||||||
} else {
|
} else {
|
||||||
ui.add(
|
ui.add(
|
||||||
&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
|
&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
|
||||||
.size(pfp_size),
|
.size(pfp_size)
|
||||||
);
|
.sense(Sense::click()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) {
|
fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) -> Option<Response> {
|
||||||
match top {
|
match top {
|
||||||
Route::Timeline(kind) => match kind {
|
Route::Timeline(kind) => match kind {
|
||||||
TimelineKind::Hashtag(_ht) => {
|
TimelineKind::Hashtag(_ht) => Some(
|
||||||
ui.add(
|
ui.add(
|
||||||
egui::Image::new(egui::include_image!(
|
egui::Image::new(egui::include_image!(
|
||||||
"../../../../../assets/icons/hashtag_icon_4x.png"
|
"../../../../../assets/icons/hashtag_icon_4x.png"
|
||||||
))
|
))
|
||||||
.fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
|
.fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
|
||||||
);
|
),
|
||||||
}
|
),
|
||||||
|
|
||||||
TimelineKind::Profile(pubkey) => {
|
TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
|
||||||
self.show_profile(ui, pubkey, pfp_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineKind::Thread(_) => {
|
TimelineKind::Thread(_) => {
|
||||||
// no pfp for threads
|
// no pfp for threads
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineKind::Search(_sq) => {
|
TimelineKind::Search(_sq) => {
|
||||||
// TODO: show author pfp if author field set?
|
// TODO: show author pfp if author field set?
|
||||||
|
|
||||||
ui.add(ui::side_panel::search_button());
|
Some(ui.add(ui::side_panel::search_button()))
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineKind::Universe
|
TimelineKind::Universe
|
||||||
| TimelineKind::Algo(_)
|
| TimelineKind::Algo(_)
|
||||||
| TimelineKind::Notifications(_)
|
| TimelineKind::Notifications(_)
|
||||||
| TimelineKind::Generic(_)
|
| TimelineKind::Generic(_)
|
||||||
| TimelineKind::List(_) => {
|
| TimelineKind::List(_) => Some(self.timeline_pfp(ui, kind, pfp_size)),
|
||||||
self.timeline_pfp(ui, kind, pfp_size);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Route::Reply(_) => {}
|
Route::Reply(_) => None,
|
||||||
Route::Quote(_) => {}
|
Route::Quote(_) => None,
|
||||||
Route::Accounts(_as) => {}
|
Route::Accounts(_as) => None,
|
||||||
Route::ComposeNote => {}
|
Route::ComposeNote => None,
|
||||||
Route::AddColumn(_add_col_route) => {}
|
Route::AddColumn(_add_col_route) => None,
|
||||||
Route::Support => {}
|
Route::Support => None,
|
||||||
Route::Relays => {}
|
Route::Relays => None,
|
||||||
Route::NewDeck => {}
|
Route::NewDeck => None,
|
||||||
Route::EditDeck(_) => {}
|
Route::EditDeck(_) => None,
|
||||||
Route::EditProfile(pubkey) => {
|
Route::EditProfile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
|
||||||
self.show_profile(ui, pubkey, pfp_size);
|
Route::Search => Some(ui.add(ui::side_panel::search_button())),
|
||||||
}
|
Route::Wallet(_) => None,
|
||||||
Route::Search => {
|
Route::CustomizeZapAmount(_) => None,
|
||||||
ui.add(ui::side_panel::search_button());
|
|
||||||
}
|
|
||||||
Route::Wallet(_) => {}
|
|
||||||
Route::CustomizeZapAmount(_) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_profile(&mut self, ui: &mut egui::Ui, pubkey: &Pubkey, pfp_size: f32) {
|
fn show_profile(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
pubkey: &Pubkey,
|
||||||
|
pfp_size: f32,
|
||||||
|
) -> egui::Response {
|
||||||
let txn = Transaction::new(self.ndb).unwrap();
|
let txn = Transaction::new(self.ndb).unwrap();
|
||||||
if let Some(mut pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) {
|
if let Some(mut pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) {
|
||||||
ui.add(&mut pfp);
|
ui.add(&mut pfp)
|
||||||
} else {
|
} else {
|
||||||
ui.add(
|
ui.add(
|
||||||
&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
|
&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
|
||||||
.size(pfp_size),
|
.size(pfp_size)
|
||||||
);
|
.sense(Sense::click()),
|
||||||
};
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title_label_value(title: &str) -> egui::Label {
|
fn title_label_value(title: &str) -> egui::Label {
|
||||||
@@ -489,27 +497,26 @@ impl<'a> NavTitle<'a> {
|
|||||||
let column_title = top.title();
|
let column_title = top.title();
|
||||||
|
|
||||||
match &column_title {
|
match &column_title {
|
||||||
ColumnTitle::Simple(title) => {
|
ColumnTitle::Simple(title) => ui.add(Self::title_label_value(title)),
|
||||||
ui.add(Self::title_label_value(title));
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnTitle::NeedsDb(need_db) => {
|
ColumnTitle::NeedsDb(need_db) => {
|
||||||
let txn = Transaction::new(self.ndb).unwrap();
|
let txn = Transaction::new(self.ndb).unwrap();
|
||||||
let title = need_db.title(&txn, self.ndb);
|
let title = need_db.title(&txn, self.ndb);
|
||||||
ui.add(Self::title_label_value(title));
|
ui.add(Self::title_label_value(title))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title(&mut self, ui: &mut egui::Ui, top: &Route, navigating: bool) -> Option<TitleResponse> {
|
fn title(&mut self, ui: &mut egui::Ui, top: &Route, navigating: bool) -> Option<TitleResponse> {
|
||||||
if !navigating {
|
let title_r = if !navigating {
|
||||||
self.title_presentation(ui, top, 32.0);
|
self.title_presentation(ui, top, 32.0)
|
||||||
}
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
if navigating {
|
if navigating {
|
||||||
self.title_presentation(ui, top, 32.0);
|
self.title_presentation(ui, top, 32.0)
|
||||||
None
|
|
||||||
} else {
|
} else {
|
||||||
let move_col = self.move_button_section(ui);
|
let move_col = self.move_button_section(ui);
|
||||||
let remove_col = self.delete_button_section(ui);
|
let remove_col = self.delete_button_section(ui);
|
||||||
@@ -523,16 +530,37 @@ impl<'a> NavTitle<'a> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.inner
|
.inner
|
||||||
|
.or(title_r)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_presentation(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
top: &Route,
|
||||||
|
pfp_size: f32,
|
||||||
|
) -> Option<TitleResponse> {
|
||||||
|
let pfp_r = self.title_pfp(ui, top, pfp_size);
|
||||||
|
|
||||||
|
if pfp_r.as_ref().is_some_and(|r| r.hovered()) {
|
||||||
|
notedeck_ui::show_pointer(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title_presentation(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) {
|
|
||||||
self.title_pfp(ui, top, pfp_size);
|
|
||||||
self.title_label(ui, top);
|
self.title_label(ui, top);
|
||||||
|
|
||||||
|
pfp_r.and_then(|r| {
|
||||||
|
if r.clicked() {
|
||||||
|
Some(TitleResponse::PfpClicked)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum TitleResponse {
|
enum TitleResponse {
|
||||||
RemoveColumn,
|
RemoveColumn,
|
||||||
|
PfpClicked,
|
||||||
MoveColumn(usize),
|
MoveColumn(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,11 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
let pfp_resp = ui.put(rect, &mut pfp);
|
let pfp_resp = ui.put(rect, &mut pfp);
|
||||||
|
|
||||||
action = action.or(pfp.action);
|
action = action.or(pfp.action);
|
||||||
|
|
||||||
|
if resp.hovered() || resp.clicked() {
|
||||||
|
crate::show_pointer(ui);
|
||||||
|
}
|
||||||
|
|
||||||
pfp_resp.on_hover_ui_at_pointer(|ui| {
|
pfp_resp.on_hover_ui_at_pointer(|ui| {
|
||||||
ui.set_max_width(300.0);
|
ui.set_max_width(300.0);
|
||||||
ui.add(ProfilePreview::new(
|
ui.add(ProfilePreview::new(
|
||||||
@@ -277,10 +282,6 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
if resp.hovered() || resp.clicked() {
|
|
||||||
crate::show_pointer(ui);
|
|
||||||
}
|
|
||||||
|
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ pub struct ProfilePic<'cache, 'url> {
|
|||||||
cache: &'cache mut Images,
|
cache: &'cache mut Images,
|
||||||
url: &'url str,
|
url: &'url str,
|
||||||
size: f32,
|
size: f32,
|
||||||
|
sense: Sense,
|
||||||
border: Option<Stroke>,
|
border: Option<Stroke>,
|
||||||
pub action: Option<MediaAction>,
|
pub action: Option<MediaAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl egui::Widget for &mut ProfilePic<'_, '_> {
|
impl egui::Widget for &mut ProfilePic<'_, '_> {
|
||||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
let inner = render_pfp(ui, self.cache, self.url, self.size, self.border);
|
let inner = render_pfp(ui, self.cache, self.url, self.size, self.border, self.sense);
|
||||||
|
|
||||||
self.action = inner.inner;
|
self.action = inner.inner;
|
||||||
|
|
||||||
@@ -26,8 +27,11 @@ impl egui::Widget for &mut ProfilePic<'_, '_> {
|
|||||||
impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
||||||
pub fn new(cache: &'cache mut Images, url: &'url str) -> Self {
|
pub fn new(cache: &'cache mut Images, url: &'url str) -> Self {
|
||||||
let size = Self::default_size() as f32;
|
let size = Self::default_size() as f32;
|
||||||
|
let sense = Sense::hover();
|
||||||
|
|
||||||
ProfilePic {
|
ProfilePic {
|
||||||
cache,
|
cache,
|
||||||
|
sense,
|
||||||
url,
|
url,
|
||||||
size,
|
size,
|
||||||
border: None,
|
border: None,
|
||||||
@@ -35,6 +39,11 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sense(mut self, sense: Sense) -> Self {
|
||||||
|
self.sense = sense;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn border_stroke(ui: &egui::Ui) -> Stroke {
|
pub fn border_stroke(ui: &egui::Ui) -> Stroke {
|
||||||
Stroke::new(4.0, ui.visuals().panel_fill)
|
Stroke::new(4.0, ui.visuals().panel_fill)
|
||||||
}
|
}
|
||||||
@@ -98,6 +107,7 @@ fn render_pfp(
|
|||||||
url: &str,
|
url: &str,
|
||||||
ui_size: f32,
|
ui_size: f32,
|
||||||
border: Option<Stroke>,
|
border: Option<Stroke>,
|
||||||
|
sense: Sense,
|
||||||
) -> InnerResponse<Option<MediaAction>> {
|
) -> InnerResponse<Option<MediaAction>> {
|
||||||
// We will want to downsample these so it's not blurry on hi res displays
|
// We will want to downsample these so it's not blurry on hi res displays
|
||||||
let img_size = 128u32;
|
let img_size = 128u32;
|
||||||
@@ -105,7 +115,6 @@ fn render_pfp(
|
|||||||
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
|
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
|
||||||
.unwrap_or(notedeck::MediaCacheType::Image);
|
.unwrap_or(notedeck::MediaCacheType::Image);
|
||||||
|
|
||||||
egui::Frame::NONE.show(ui, |ui| {
|
|
||||||
let cur_state = get_render_state(
|
let cur_state = get_render_state(
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
img_cache,
|
img_cache,
|
||||||
@@ -116,28 +125,29 @@ fn render_pfp(
|
|||||||
|
|
||||||
match cur_state.texture_state {
|
match cur_state.texture_state {
|
||||||
notedeck::TextureState::Pending => {
|
notedeck::TextureState::Pending => {
|
||||||
paint_circle(ui, ui_size, border);
|
egui::InnerResponse::new(None, paint_circle(ui, ui_size, border, sense))
|
||||||
None
|
|
||||||
}
|
}
|
||||||
notedeck::TextureState::Error(e) => {
|
notedeck::TextureState::Error(e) => {
|
||||||
paint_circle(ui, ui_size, border);
|
let r = paint_circle(ui, ui_size, border, sense);
|
||||||
show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}"));
|
show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}"));
|
||||||
|
egui::InnerResponse::new(
|
||||||
Some(MediaAction::FetchImage {
|
Some(MediaAction::FetchImage {
|
||||||
url: url.to_owned(),
|
url: url.to_owned(),
|
||||||
cache_type,
|
cache_type,
|
||||||
no_pfp_promise: fetch_no_pfp_promise(ui.ctx(), img_cache.get_cache(cache_type)),
|
no_pfp_promise: fetch_no_pfp_promise(ui.ctx(), img_cache.get_cache(cache_type)),
|
||||||
})
|
}),
|
||||||
|
r,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
notedeck::TextureState::Loaded(textured_image) => {
|
notedeck::TextureState::Loaded(textured_image) => {
|
||||||
let texture_handle = handle_repaint(
|
let texture_handle = handle_repaint(
|
||||||
ui,
|
ui,
|
||||||
retrieve_latest_texture(url, cur_state.gifs, textured_image),
|
retrieve_latest_texture(url, cur_state.gifs, textured_image),
|
||||||
);
|
);
|
||||||
pfp_image(ui, texture_handle, ui_size, border);
|
|
||||||
None
|
egui::InnerResponse::new(None, pfp_image(ui, texture_handle, ui_size, border, sense))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[profiling::function]
|
#[profiling::function]
|
||||||
@@ -146,8 +156,9 @@ fn pfp_image(
|
|||||||
img: &TextureHandle,
|
img: &TextureHandle,
|
||||||
size: f32,
|
size: f32,
|
||||||
border: Option<Stroke>,
|
border: Option<Stroke>,
|
||||||
|
sense: Sense,
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover());
|
let (rect, response) = ui.allocate_at_least(vec2(size, size), sense);
|
||||||
if let Some(stroke) = border {
|
if let Some(stroke) = border {
|
||||||
draw_bg_border(ui, rect.center(), size, stroke);
|
draw_bg_border(ui, rect.center(), size, stroke);
|
||||||
}
|
}
|
||||||
@@ -156,8 +167,13 @@ fn pfp_image(
|
|||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_circle(ui: &mut egui::Ui, size: f32, border: Option<Stroke>) -> egui::Response {
|
fn paint_circle(
|
||||||
let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover());
|
ui: &mut egui::Ui,
|
||||||
|
size: f32,
|
||||||
|
border: Option<Stroke>,
|
||||||
|
sense: Sense,
|
||||||
|
) -> egui::Response {
|
||||||
|
let (rect, response) = ui.allocate_at_least(vec2(size, size), sense);
|
||||||
|
|
||||||
if let Some(stroke) = border {
|
if let Some(stroke) = border {
|
||||||
draw_bg_border(ui, rect.center(), size, stroke);
|
draw_bg_border(ui, rect.center(), size, stroke);
|
||||||
|
|||||||
Reference in New Issue
Block a user