Merge 'column titlebar #345'
William Casarin (2):
update to use upstream egui-nav branch
kernelkind (13):
basic add column impl
remote sub new timeline
add more add column options
animate add column options
push column picker immediately to new column
move get first router to Columns
tmp use kernelkind egui-nav
title bar
unsubscribe timeline on deletion
fix deck author bug & rename titles
tmp: kernelkind/egui-nav
updated back arrow
tmp: kernelkind/egui-nav
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -1148,7 +1148,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_nav"
|
name = "egui_nav"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/damus-io/egui-nav?rev=b19742503329a13df660ac8c5a3ada4a25b7cc53#b19742503329a13df660ac8c5a3ada4a25b7cc53"
|
source = "git+https://github.com/damus-io/egui-nav?rev=6ba42de2bae384d10e35c532f3856b81d2e9f645#6ba42de2bae384d10e35c532f3856b81d2e9f645"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
@@ -1660,7 +1660,7 @@ checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"gpu-descriptor-types",
|
"gpu-descriptor-types",
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1692,6 +1692,12 @@ dependencies = [
|
|||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hassle-rs"
|
name = "hassle-rs"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -1950,12 +1956,12 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.5.0"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.0",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2508,6 +2514,7 @@ dependencies = [
|
|||||||
"env_logger 0.10.2",
|
"env_logger 0.10.2",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"poll-promise",
|
"poll-promise",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da
|
|||||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
|
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
|
||||||
ehttp = "0.2.0"
|
ehttp = "0.2.0"
|
||||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" }
|
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" }
|
||||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "b19742503329a13df660ac8c5a3ada4a25b7cc53" }
|
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "6ba42de2bae384d10e35c532f3856b81d2e9f645" }
|
||||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" }
|
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" }
|
||||||
reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] }
|
reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] }
|
||||||
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
||||||
@@ -42,6 +42,7 @@ strum = "0.26"
|
|||||||
strum_macros = "0.26"
|
strum_macros = "0.26"
|
||||||
bitflags = "2.5.0"
|
bitflags = "2.5.0"
|
||||||
uuid = { version = "1.10.0", features = ["v4"] }
|
uuid = { version = "1.10.0", features = ["v4"] }
|
||||||
|
indexmap = "2.6.0"
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
|||||||
BIN
assets/icons/column_delete_icon_4x.png
Normal file
BIN
assets/icons/column_delete_icon_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 B |
BIN
assets/icons/home_icon_dark_4x.png
Normal file
BIN
assets/icons/home_icon_dark_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/icons/notifications_icon_dark_4x.png
Normal file
BIN
assets/icons/notifications_icon_dark_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/icons/universe_icon_dark_4x.png
Normal file
BIN
assets/icons/universe_icon_dark_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
168
src/app.rs
168
src/app.rs
@@ -3,21 +3,19 @@ use crate::{
|
|||||||
app_creation::setup_cc,
|
app_creation::setup_cc,
|
||||||
app_style::user_requested_visuals_change,
|
app_style::user_requested_visuals_change,
|
||||||
args::Args,
|
args::Args,
|
||||||
column::{Column, Columns},
|
column::Columns,
|
||||||
draft::Drafts,
|
draft::Drafts,
|
||||||
error::{Error, FilterError},
|
error::{Error, FilterError},
|
||||||
filter,
|
filter::{self, FilterState},
|
||||||
filter::FilterState,
|
|
||||||
frame_history::FrameHistory,
|
frame_history::FrameHistory,
|
||||||
imgcache::ImageCache,
|
imgcache::ImageCache,
|
||||||
key_storage::KeyStorageType,
|
key_storage::KeyStorageType,
|
||||||
nav,
|
nav,
|
||||||
note::NoteRef,
|
note::NoteRef,
|
||||||
notecache::{CachedNote, NoteCache},
|
notecache::{CachedNote, NoteCache},
|
||||||
route::Route,
|
|
||||||
subscriptions::{SubKind, Subscriptions},
|
subscriptions::{SubKind, Subscriptions},
|
||||||
thread::Threads,
|
thread::Threads,
|
||||||
timeline::{Timeline, TimelineKind, ViewFilter},
|
timeline::{Timeline, TimelineId, TimelineKind, ViewFilter},
|
||||||
ui::{self, DesktopSidePanel},
|
ui::{self, DesktopSidePanel},
|
||||||
unknowns::UnknownIds,
|
unknowns::UnknownIds,
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
@@ -41,6 +39,7 @@ use tracing::{debug, error, info, trace, warn};
|
|||||||
pub enum DamusState {
|
pub enum DamusState {
|
||||||
Initializing,
|
Initializing,
|
||||||
Initialized,
|
Initialized,
|
||||||
|
NewTimelineSub(TimelineId),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||||
@@ -248,7 +247,7 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
|
|||||||
|
|
||||||
if let Err(err) = Timeline::poll_notes_into_view(
|
if let Err(err) = Timeline::poll_notes_into_view(
|
||||||
timeline_ind,
|
timeline_ind,
|
||||||
&mut damus.columns.timelines,
|
damus.columns.timelines_mut(),
|
||||||
&damus.ndb,
|
&damus.ndb,
|
||||||
&txn,
|
&txn,
|
||||||
&mut damus.unknown_ids,
|
&mut damus.unknown_ids,
|
||||||
@@ -394,47 +393,105 @@ fn setup_initial_nostrdb_subs(
|
|||||||
columns: &mut Columns,
|
columns: &mut Columns,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for timeline in columns.timelines_mut() {
|
for timeline in columns.timelines_mut() {
|
||||||
match &timeline.filter {
|
setup_nostrdb_sub(ndb, note_cache, timeline)?
|
||||||
FilterState::Ready(filters) => {
|
}
|
||||||
{ setup_initial_timeline(ndb, timeline, note_cache, &filters.clone()) }?
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterState::Broken(err) => {
|
Ok(())
|
||||||
error!("FetchingRemote state broken in setup_initial_nostr_subs: {err}")
|
}
|
||||||
}
|
|
||||||
FilterState::FetchingRemote(_) => {
|
fn setup_nostrdb_sub(ndb: &Ndb, note_cache: &mut NoteCache, timeline: &mut Timeline) -> Result<()> {
|
||||||
error!("FetchingRemote state in setup_initial_nostr_subs")
|
match &timeline.filter {
|
||||||
}
|
FilterState::Ready(filters) => {
|
||||||
FilterState::GotRemote(_) => {
|
{ setup_initial_timeline(ndb, timeline, note_cache, &filters.clone()) }?
|
||||||
error!("GotRemote state in setup_initial_nostr_subs")
|
}
|
||||||
}
|
|
||||||
FilterState::NeedsRemote(_filters) => {
|
FilterState::Broken(err) => {
|
||||||
// can't do anything yet, we defer to first connect to send
|
error!("FetchingRemote state broken in setup_initial_nostr_subs: {err}")
|
||||||
// remote filters
|
}
|
||||||
}
|
FilterState::FetchingRemote(_) => {
|
||||||
|
error!("FetchingRemote state in setup_initial_nostr_subs")
|
||||||
|
}
|
||||||
|
FilterState::GotRemote(_) => {
|
||||||
|
error!("GotRemote state in setup_initial_nostr_subs")
|
||||||
|
}
|
||||||
|
FilterState::NeedsRemote(_filters) => {
|
||||||
|
// can't do anything yet, we defer to first connect to send
|
||||||
|
// remote filters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
|
fn setup_new_nostrdb_sub(
|
||||||
if damus.state == DamusState::Initializing {
|
ndb: &Ndb,
|
||||||
#[cfg(feature = "profiling")]
|
note_cache: &mut NoteCache,
|
||||||
setup_profiling();
|
columns: &mut Columns,
|
||||||
|
new_timeline_id: TimelineId,
|
||||||
damus.state = DamusState::Initialized;
|
) -> Result<()> {
|
||||||
// this lets our eose handler know to close unknownids right away
|
if let Some(timeline) = columns.find_timeline_mut(new_timeline_id) {
|
||||||
damus
|
info!("Setting up timeline sub for {}", timeline.id);
|
||||||
.subscriptions()
|
if let FilterState::Ready(filters) = &timeline.filter {
|
||||||
.insert("unknownids".to_string(), SubKind::OneShot);
|
for filter in filters {
|
||||||
setup_initial_nostrdb_subs(&damus.ndb, &mut damus.note_cache, &mut damus.columns)
|
info!("Setting up filter {:?}", filter.json());
|
||||||
.expect("home subscription failed");
|
}
|
||||||
|
}
|
||||||
|
setup_nostrdb_sub(ndb, note_cache, timeline)?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
|
||||||
|
match damus.state {
|
||||||
|
DamusState::Initializing => {
|
||||||
|
#[cfg(feature = "profiling")]
|
||||||
|
setup_profiling();
|
||||||
|
|
||||||
|
damus.state = DamusState::Initialized;
|
||||||
|
// this lets our eose handler know to close unknownids right away
|
||||||
|
damus
|
||||||
|
.subscriptions()
|
||||||
|
.insert("unknownids".to_string(), SubKind::OneShot);
|
||||||
|
setup_initial_nostrdb_subs(&damus.ndb, &mut damus.note_cache, &mut damus.columns)
|
||||||
|
.expect("home subscription failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
DamusState::NewTimelineSub(new_timeline_id) => {
|
||||||
|
info!("adding new timeline {}", new_timeline_id);
|
||||||
|
setup_new_nostrdb_sub(
|
||||||
|
&damus.ndb,
|
||||||
|
&mut damus.note_cache,
|
||||||
|
&mut damus.columns,
|
||||||
|
new_timeline_id,
|
||||||
|
)
|
||||||
|
.expect("new timeline subscription failed");
|
||||||
|
|
||||||
|
if let Some(filter) = {
|
||||||
|
let timeline = damus
|
||||||
|
.columns
|
||||||
|
.find_timeline(new_timeline_id)
|
||||||
|
.expect("timeline");
|
||||||
|
match &timeline.filter {
|
||||||
|
FilterState::Ready(filters) => Some(filters.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
let subid = Uuid::new_v4().to_string();
|
||||||
|
damus.pool.subscribe(subid, filter);
|
||||||
|
|
||||||
|
damus.state = DamusState::Initialized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DamusState::Initialized => (),
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(err) = try_process_event(damus, ctx) {
|
if let Err(err) = try_process_event(damus, ctx) {
|
||||||
error!("error processing event: {}", err);
|
error!("error processing event: {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
damus.columns.attempt_perform_deletion_request();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_event(damus: &mut Damus, _subid: &str, event: &str) {
|
fn process_event(damus: &mut Damus, _subid: &str, event: &str) {
|
||||||
@@ -643,11 +700,7 @@ impl Damus {
|
|||||||
let debug = parsed_args.debug;
|
let debug = parsed_args.debug;
|
||||||
|
|
||||||
if columns.columns().is_empty() {
|
if columns.columns().is_empty() {
|
||||||
let filter = Filter::from_json(include_str!("../queries/timeline.json")).unwrap();
|
columns.new_column_picker();
|
||||||
columns.add_timeline(Timeline::new(
|
|
||||||
TimelineKind::Generic,
|
|
||||||
FilterState::ready(vec![filter]),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -714,6 +767,10 @@ impl Damus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_new_timeline(&mut self, timeline_id: TimelineId) {
|
||||||
|
self.state = DamusState::NewTimelineSub(timeline_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
|
pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
|
||||||
let mut columns = Columns::new();
|
let mut columns = Columns::new();
|
||||||
let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap();
|
let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap();
|
||||||
@@ -897,7 +954,7 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
|
|||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
let screen_size = ctx.screen_rect().width();
|
let screen_size = ctx.screen_rect().width();
|
||||||
let calc_panel_width = (screen_size / app.columns.columns().len() as f32) - 30.0;
|
let calc_panel_width = (screen_size / app.columns.num_columns() as f32) - 30.0;
|
||||||
let min_width = 320.0;
|
let min_width = 320.0;
|
||||||
let need_scroll = calc_panel_width < min_width;
|
let need_scroll = calc_panel_width < min_width;
|
||||||
let panel_sizes = if need_scroll {
|
let panel_sizes = if need_scroll {
|
||||||
@@ -910,18 +967,18 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
|
|||||||
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().show(ui, |ui| {
|
||||||
timelines_view(ui, panel_sizes, app, app.columns.columns().len());
|
timelines_view(ui, panel_sizes, app);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
timelines_view(ui, panel_sizes, app, app.columns.columns().len());
|
timelines_view(ui, panel_sizes, app);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usize) {
|
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH))
|
.size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH))
|
||||||
.sizes(sizes, columns)
|
.sizes(sizes, app.columns.num_columns())
|
||||||
.clip(true)
|
.clip(true)
|
||||||
.horizontal(|mut strip| {
|
.horizontal(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
@@ -933,22 +990,8 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz
|
|||||||
)
|
)
|
||||||
.show(ui);
|
.show(ui);
|
||||||
|
|
||||||
let router = if let Some(router) = app
|
|
||||||
.columns
|
|
||||||
.columns_mut()
|
|
||||||
.get_mut(0)
|
|
||||||
.map(|c: &mut Column| c.router_mut())
|
|
||||||
{
|
|
||||||
router
|
|
||||||
} else {
|
|
||||||
// TODO(jb55): Maybe we should have an empty column route?
|
|
||||||
let columns = app.columns.columns_mut();
|
|
||||||
columns.push(Column::new(vec![Route::accounts()]));
|
|
||||||
columns[0].router_mut()
|
|
||||||
};
|
|
||||||
|
|
||||||
if side_panel.response.clicked() {
|
if side_panel.response.clicked() {
|
||||||
DesktopSidePanel::perform_action(router, side_panel.action);
|
DesktopSidePanel::perform_action(app.columns_mut(), side_panel.action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// vertical sidebar line
|
// vertical sidebar line
|
||||||
@@ -959,11 +1002,10 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let n_cols = app.columns.columns().len();
|
for col_index in 0..app.columns.num_columns() {
|
||||||
for column_ind in 0..n_cols {
|
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
let rect = ui.available_rect_before_wrap();
|
let rect = ui.available_rect_before_wrap();
|
||||||
nav::render_nav(column_ind, app, ui);
|
nav::render_nav(col_index, app, ui);
|
||||||
|
|
||||||
// vertical line
|
// vertical line
|
||||||
ui.painter().vline(
|
ui.painter().vline(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::colors::{
|
use crate::{
|
||||||
desktop_dark_color_theme, light_color_theme, mobile_dark_color_theme, ColorTheme,
|
colors::{desktop_dark_color_theme, light_color_theme, mobile_dark_color_theme, ColorTheme},
|
||||||
|
ui::is_narrow,
|
||||||
};
|
};
|
||||||
use egui::{
|
use egui::{
|
||||||
epaint::Shadow,
|
epaint::Shadow,
|
||||||
@@ -96,6 +97,14 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32 {
|
||||||
|
if is_narrow(ctx) {
|
||||||
|
mobile_font_size(text_style)
|
||||||
|
} else {
|
||||||
|
desktop_font_size(text_style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, EnumIter)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, EnumIter)]
|
||||||
pub enum NotedeckTextStyle {
|
pub enum NotedeckTextStyle {
|
||||||
Heading,
|
Heading,
|
||||||
|
|||||||
122
src/column.rs
122
src/column.rs
@@ -1,6 +1,8 @@
|
|||||||
use crate::route::{Route, Router};
|
use crate::route::{Route, Router};
|
||||||
use crate::timeline::{Timeline, TimelineId};
|
use crate::timeline::{Timeline, TimelineId};
|
||||||
|
use indexmap::IndexMap;
|
||||||
use std::iter::Iterator;
|
use std::iter::Iterator;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
pub struct Column {
|
pub struct Column {
|
||||||
@@ -25,16 +27,18 @@ impl Column {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Columns {
|
pub struct Columns {
|
||||||
/// Columns are simply routers into settings, timelines, etc
|
/// Columns are simply routers into settings, timelines, etc
|
||||||
columns: Vec<Column>,
|
columns: IndexMap<u32, Column>,
|
||||||
|
|
||||||
/// Timeline state is not tied to routing logic separately, so that
|
/// Timeline state is not tied to routing logic separately, so that
|
||||||
/// different columns can navigate to and from settings to timelines,
|
/// different columns can navigate to and from settings to timelines,
|
||||||
/// etc.
|
/// etc.
|
||||||
pub timelines: Vec<Timeline>,
|
pub timelines: IndexMap<u32, Timeline>,
|
||||||
|
|
||||||
/// The selected column for key navigation
|
/// The selected column for key navigation
|
||||||
selected: i32,
|
selected: i32,
|
||||||
|
should_delete_column_at_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
static UIDS: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
impl Columns {
|
impl Columns {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -42,49 +46,112 @@ impl Columns {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_timeline(&mut self, timeline: Timeline) {
|
pub fn add_timeline(&mut self, timeline: Timeline) {
|
||||||
|
let id = Self::get_new_id();
|
||||||
let routes = vec![Route::timeline(timeline.id)];
|
let routes = vec![Route::timeline(timeline.id)];
|
||||||
self.timelines.push(timeline);
|
self.timelines.insert(id, timeline);
|
||||||
self.columns.push(Column::new(routes))
|
self.columns.insert(id, Column::new(routes));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn columns_mut(&mut self) -> &mut Vec<Column> {
|
pub fn add_timeline_to_column(&mut self, col: usize, timeline: Timeline) {
|
||||||
&mut self.columns
|
let col_id = self.get_column_id_at_index(col);
|
||||||
|
self.column_mut(col)
|
||||||
|
.router_mut()
|
||||||
|
.route_to_replaced(Route::timeline(timeline.id));
|
||||||
|
self.timelines.insert(col_id, timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_column_picker(&mut self) {
|
||||||
|
self.add_column(Column::new(vec![Route::AddColumn]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_new_id() -> u32 {
|
||||||
|
UIDS.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_column(&mut self, column: Column) {
|
||||||
|
self.columns.insert(Self::get_new_id(), column);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn columns_mut(&mut self) -> Vec<&mut Column> {
|
||||||
|
self.columns.values_mut().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_columns(&self) -> usize {
|
||||||
|
self.columns.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first router in the columns if there are columns present.
|
||||||
|
// Otherwise, create a new column picker and return the router
|
||||||
|
pub fn get_first_router(&mut self) -> &mut Router<Route> {
|
||||||
|
if self.columns.is_empty() {
|
||||||
|
self.new_column_picker();
|
||||||
|
}
|
||||||
|
self.columns
|
||||||
|
.get_index_mut(0)
|
||||||
|
.expect("There should be at least one column")
|
||||||
|
.1
|
||||||
|
.router_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timeline_mut(&mut self, timeline_ind: usize) -> &mut Timeline {
|
pub fn timeline_mut(&mut self, timeline_ind: usize) -> &mut Timeline {
|
||||||
&mut self.timelines[timeline_ind]
|
self.timelines
|
||||||
|
.get_index_mut(timeline_ind)
|
||||||
|
.expect("expected index to be in bounds")
|
||||||
|
.1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column(&self, ind: usize) -> &Column {
|
pub fn column(&self, ind: usize) -> &Column {
|
||||||
&self.columns()[ind]
|
self.columns
|
||||||
|
.get_index(ind)
|
||||||
|
.expect("Expected index to be in bounds")
|
||||||
|
.1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn columns(&self) -> &Vec<Column> {
|
pub fn columns(&self) -> Vec<&Column> {
|
||||||
&self.columns
|
self.columns.values().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_column_id_at_index(&self, ind: usize) -> u32 {
|
||||||
|
*self
|
||||||
|
.columns
|
||||||
|
.get_index(ind)
|
||||||
|
.expect("expected index to be within bounds")
|
||||||
|
.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected(&mut self) -> &mut Column {
|
pub fn selected(&mut self) -> &mut Column {
|
||||||
&mut self.columns[self.selected as usize]
|
self.columns
|
||||||
|
.get_index_mut(self.selected as usize)
|
||||||
|
.expect("Expected selected index to be in bounds")
|
||||||
|
.1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timelines_mut(&mut self) -> &mut Vec<Timeline> {
|
pub fn timelines_mut(&mut self) -> Vec<&mut Timeline> {
|
||||||
&mut self.timelines
|
self.timelines.values_mut().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timelines(&self) -> &Vec<Timeline> {
|
pub fn timelines(&self) -> Vec<&Timeline> {
|
||||||
&self.timelines
|
self.timelines.values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> {
|
pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> {
|
||||||
self.timelines_mut().iter_mut().find(|tl| tl.id == id)
|
self.timelines_mut().into_iter().find(|tl| tl.id == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> {
|
pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> {
|
||||||
self.timelines().iter().find(|tl| tl.id == id)
|
self.timelines().into_iter().find(|tl| tl.id == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_mut(&mut self, ind: usize) -> &mut Column {
|
pub fn column_mut(&mut self, ind: usize) -> &mut Column {
|
||||||
&mut self.columns[ind]
|
self.columns
|
||||||
|
.get_index_mut(ind)
|
||||||
|
.expect("Expected index to be in bounds")
|
||||||
|
.1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_timeline_for_column_index(&self, ind: usize) -> Option<&Timeline> {
|
||||||
|
let col_id = self.get_column_id_at_index(ind);
|
||||||
|
self.timelines.get(&col_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_down(&mut self) {
|
pub fn select_down(&mut self) {
|
||||||
@@ -108,4 +175,23 @@ impl Columns {
|
|||||||
}
|
}
|
||||||
self.selected += 1;
|
self.selected += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_deletion_at_index(&mut self, index: usize) {
|
||||||
|
self.should_delete_column_at_index = Some(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attempt_perform_deletion_request(&mut self) {
|
||||||
|
if let Some(index) = self.should_delete_column_at_index {
|
||||||
|
if let Some((key, _)) = self.columns.get_index_mut(index) {
|
||||||
|
self.timelines.shift_remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.columns.shift_remove_index(index);
|
||||||
|
self.should_delete_column_at_index = None;
|
||||||
|
|
||||||
|
if self.columns.is_empty() {
|
||||||
|
self.new_column_picker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/fonts.rs
15
src/fonts.rs
@@ -4,12 +4,13 @@ use tracing::debug;
|
|||||||
|
|
||||||
pub enum NamedFontFamily {
|
pub enum NamedFontFamily {
|
||||||
Medium,
|
Medium,
|
||||||
|
Bold,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NamedFontFamily {
|
impl NamedFontFamily {
|
||||||
pub fn as_str(&mut self) -> &'static str {
|
pub fn as_str(&mut self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
//Self::Bold => "bold",
|
Self::Bold => "bold",
|
||||||
Self::Medium => "medium",
|
Self::Medium => "medium",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ pub fn setup_fonts(ctx: &egui::Context) {
|
|||||||
"DejaVuSans".to_owned(),
|
"DejaVuSans".to_owned(),
|
||||||
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||||
);
|
);
|
||||||
/*
|
|
||||||
font_data.insert(
|
font_data.insert(
|
||||||
"OnestBold".to_owned(),
|
"OnestBold".to_owned(),
|
||||||
FontData::from_static(include_bytes!(
|
FontData::from_static(include_bytes!(
|
||||||
@@ -51,6 +52,7 @@ pub fn setup_fonts(ctx: &egui::Context) {
|
|||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
font_data.insert(
|
font_data.insert(
|
||||||
"DejaVuSansBold".to_owned(),
|
"DejaVuSansBold".to_owned(),
|
||||||
FontData::from_static(include_bytes!(
|
FontData::from_static(include_bytes!(
|
||||||
@@ -119,7 +121,10 @@ pub fn setup_fonts(ctx: &egui::Context) {
|
|||||||
medium.extend(base_fonts.clone());
|
medium.extend(base_fonts.clone());
|
||||||
|
|
||||||
let mut mono = vec!["Inconsolata".to_owned()];
|
let mut mono = vec!["Inconsolata".to_owned()];
|
||||||
mono.extend(base_fonts);
|
mono.extend(base_fonts.clone());
|
||||||
|
|
||||||
|
let mut bold = vec!["OnestBold".to_owned()];
|
||||||
|
bold.extend(base_fonts);
|
||||||
|
|
||||||
families.insert(egui::FontFamily::Proportional, proportional);
|
families.insert(egui::FontFamily::Proportional, proportional);
|
||||||
families.insert(egui::FontFamily::Monospace, mono);
|
families.insert(egui::FontFamily::Monospace, mono);
|
||||||
@@ -127,6 +132,10 @@ pub fn setup_fonts(ctx: &egui::Context) {
|
|||||||
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
||||||
medium,
|
medium,
|
||||||
);
|
);
|
||||||
|
families.insert(
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||||
|
bold,
|
||||||
|
);
|
||||||
|
|
||||||
debug!("fonts: {:?}", families);
|
debug!("fonts: {:?}", families);
|
||||||
|
|
||||||
|
|||||||
346
src/nav.rs
346
src/nav.rs
@@ -1,84 +1,121 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
account_manager::render_accounts_route,
|
account_manager::render_accounts_route,
|
||||||
|
app_style::{get_font_size, NotedeckTextStyle},
|
||||||
|
fonts::NamedFontFamily,
|
||||||
relay_pool_manager::RelayPoolManager,
|
relay_pool_manager::RelayPoolManager,
|
||||||
route::Route,
|
route::Route,
|
||||||
thread::thread_unsubscribe,
|
thread::thread_unsubscribe,
|
||||||
timeline::route::{render_timeline_route, TimelineRoute, TimelineRouteResponse},
|
timeline::route::{render_timeline_route, AfterRouteExecution, TimelineRoute},
|
||||||
ui::{self, note::PostAction, RelayView, View},
|
ui::{
|
||||||
|
self,
|
||||||
|
add_column::{AddColumnResponse, AddColumnView},
|
||||||
|
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||||
|
note::PostAction,
|
||||||
|
RelayView, View,
|
||||||
|
},
|
||||||
Damus,
|
Damus,
|
||||||
};
|
};
|
||||||
|
|
||||||
use egui_nav::{Nav, NavAction};
|
use egui::{pos2, Color32, InnerResponse, Stroke};
|
||||||
|
use egui_nav::{Nav, NavAction, TitleBarResponse};
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) {
|
pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) {
|
||||||
|
let col_id = app.columns.get_column_id_at_index(col);
|
||||||
// TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
|
// TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
|
||||||
let nav_response = Nav::new(app.columns().column(col).router().routes().clone())
|
let routes = app
|
||||||
|
.columns()
|
||||||
|
.column(col)
|
||||||
|
.router()
|
||||||
|
.routes()
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.get_titled_route(&app.columns, &app.ndb))
|
||||||
|
.collect();
|
||||||
|
let nav_response = Nav::new(routes)
|
||||||
.navigating(app.columns_mut().column_mut(col).router_mut().navigating)
|
.navigating(app.columns_mut().column_mut(col).router_mut().navigating)
|
||||||
.returning(app.columns_mut().column_mut(col).router_mut().returning)
|
.returning(app.columns_mut().column_mut(col).router_mut().returning)
|
||||||
.title(false)
|
.title(48.0, title_bar)
|
||||||
.show_mut(ui, |ui, nav| match nav.top() {
|
.show_mut(col_id, ui, |ui, nav| {
|
||||||
Route::Timeline(tlr) => render_timeline_route(
|
let column = app.columns.column_mut(col);
|
||||||
&app.ndb,
|
match &nav.top().route {
|
||||||
&mut app.columns,
|
Route::Timeline(tlr) => render_timeline_route(
|
||||||
&mut app.pool,
|
|
||||||
&mut app.drafts,
|
|
||||||
&mut app.img_cache,
|
|
||||||
&mut app.note_cache,
|
|
||||||
&mut app.threads,
|
|
||||||
&mut app.accounts,
|
|
||||||
*tlr,
|
|
||||||
col,
|
|
||||||
app.textmode,
|
|
||||||
ui,
|
|
||||||
),
|
|
||||||
Route::Accounts(amr) => {
|
|
||||||
render_accounts_route(
|
|
||||||
ui,
|
|
||||||
&app.ndb,
|
&app.ndb,
|
||||||
col,
|
|
||||||
&mut app.columns,
|
&mut app.columns,
|
||||||
&mut app.img_cache,
|
&mut app.pool,
|
||||||
&mut app.accounts,
|
&mut app.drafts,
|
||||||
&mut app.view_state.login,
|
|
||||||
*amr,
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Route::Relays => {
|
|
||||||
let manager = RelayPoolManager::new(app.pool_mut());
|
|
||||||
RelayView::new(manager).ui(ui);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Route::ComposeNote => {
|
|
||||||
let kp = app.accounts.selected_or_first_nsec()?;
|
|
||||||
let draft = app.drafts.compose_mut();
|
|
||||||
|
|
||||||
let txn = nostrdb::Transaction::new(&app.ndb).expect("txn");
|
|
||||||
let post_response = ui::PostView::new(
|
|
||||||
&app.ndb,
|
|
||||||
draft,
|
|
||||||
crate::draft::DraftSource::Compose,
|
|
||||||
&mut app.img_cache,
|
&mut app.img_cache,
|
||||||
&mut app.note_cache,
|
&mut app.note_cache,
|
||||||
kp,
|
&mut app.threads,
|
||||||
)
|
&mut app.accounts,
|
||||||
.ui(&txn, ui);
|
*tlr,
|
||||||
|
col,
|
||||||
if let Some(action) = post_response.action {
|
app.textmode,
|
||||||
PostAction::execute(kp, &action, &mut app.pool, draft, |np, seckey| {
|
ui,
|
||||||
np.to_note(seckey)
|
),
|
||||||
});
|
Route::Accounts(amr) => {
|
||||||
app.columns_mut().column_mut(col).router_mut().go_back();
|
render_accounts_route(
|
||||||
|
ui,
|
||||||
|
&app.ndb,
|
||||||
|
col,
|
||||||
|
&mut app.columns,
|
||||||
|
&mut app.img_cache,
|
||||||
|
&mut app.accounts,
|
||||||
|
&mut app.view_state.login,
|
||||||
|
*amr,
|
||||||
|
);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
Route::Relays => {
|
||||||
|
let manager = RelayPoolManager::new(app.pool_mut());
|
||||||
|
RelayView::new(manager).ui(ui);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Route::ComposeNote => {
|
||||||
|
let kp = app.accounts.selected_or_first_nsec()?;
|
||||||
|
let draft = app.drafts.compose_mut();
|
||||||
|
|
||||||
None
|
let txn = nostrdb::Transaction::new(&app.ndb).expect("txn");
|
||||||
|
let post_response = ui::PostView::new(
|
||||||
|
&app.ndb,
|
||||||
|
draft,
|
||||||
|
crate::draft::DraftSource::Compose,
|
||||||
|
&mut app.img_cache,
|
||||||
|
&mut app.note_cache,
|
||||||
|
kp,
|
||||||
|
)
|
||||||
|
.ui(&txn, ui);
|
||||||
|
|
||||||
|
if let Some(action) = post_response.action {
|
||||||
|
PostAction::execute(kp, &action, &mut app.pool, draft, |np, seckey| {
|
||||||
|
np.to_note(seckey)
|
||||||
|
});
|
||||||
|
column.router_mut().go_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Route::AddColumn => {
|
||||||
|
let resp =
|
||||||
|
AddColumnView::new(&app.ndb, app.accounts.get_selected_account()).ui(ui);
|
||||||
|
|
||||||
|
if let Some(resp) = resp {
|
||||||
|
match resp {
|
||||||
|
AddColumnResponse::Timeline(timeline) => {
|
||||||
|
let id = timeline.id;
|
||||||
|
app.columns_mut().add_timeline_to_column(col, timeline);
|
||||||
|
app.subscribe_new_timeline(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(reply_response) = nav_response.inner {
|
if let Some(after_route_execution) = nav_response.inner {
|
||||||
// start returning when we're finished posting
|
// start returning when we're finished posting
|
||||||
match reply_response {
|
match after_route_execution {
|
||||||
TimelineRouteResponse::Post(resp) => {
|
AfterRouteExecution::Post(resp) => {
|
||||||
if let Some(action) = resp.action {
|
if let Some(action) = resp.action {
|
||||||
match action {
|
match action {
|
||||||
PostAction::Post(_) => {
|
PostAction::Post(_) => {
|
||||||
@@ -102,6 +139,197 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(NavAction::Navigated) = nav_response.action {
|
} else if let Some(NavAction::Navigated) = nav_response.action {
|
||||||
app.columns_mut().column_mut(col).router_mut().navigating = false;
|
let cur_router = app.columns_mut().column_mut(col).router_mut();
|
||||||
|
cur_router.navigating = false;
|
||||||
|
if cur_router.is_replacing() {
|
||||||
|
cur_router.remove_previous_route();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(title_response) = nav_response.title_response {
|
||||||
|
match title_response {
|
||||||
|
TitleResponse::RemoveColumn => {
|
||||||
|
app.columns_mut().request_deletion_at_index(col);
|
||||||
|
let tl = app.columns().find_timeline_for_column_index(col);
|
||||||
|
if let Some(timeline) = tl {
|
||||||
|
if let Some(sub_id) = timeline.subscription {
|
||||||
|
if let Err(e) = app.ndb.unsubscribe(sub_id) {
|
||||||
|
error!("unsubscribe error: {}", e);
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"successfully unsubscribed from timeline {} with sub id {}",
|
||||||
|
timeline.id,
|
||||||
|
sub_id.id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn title_bar(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
allocated_response: egui::Response,
|
||||||
|
title_name: String,
|
||||||
|
back_name: Option<String>,
|
||||||
|
) -> egui::InnerResponse<TitleBarResponse<TitleResponse>> {
|
||||||
|
let icon_width = 32.0;
|
||||||
|
let padding_external = 16.0;
|
||||||
|
let padding_internal = 8.0;
|
||||||
|
let has_back = back_name.is_some();
|
||||||
|
|
||||||
|
let (spacing_rect, titlebar_rect) = allocated_response
|
||||||
|
.rect
|
||||||
|
.split_left_right_at_x(allocated_response.rect.left() + padding_external);
|
||||||
|
ui.advance_cursor_after_rect(spacing_rect);
|
||||||
|
|
||||||
|
let (titlebar_resp, maybe_button_resp) = if has_back {
|
||||||
|
let (button_rect, titlebar_rect) = titlebar_rect
|
||||||
|
.split_left_right_at_x(allocated_response.rect.left() + icon_width + padding_external);
|
||||||
|
(
|
||||||
|
allocated_response.with_new_rect(titlebar_rect),
|
||||||
|
Some(back_button(ui, button_rect)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(allocated_response, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
title(
|
||||||
|
ui,
|
||||||
|
title_name,
|
||||||
|
titlebar_resp.rect,
|
||||||
|
icon_width,
|
||||||
|
if has_back {
|
||||||
|
padding_internal
|
||||||
|
} else {
|
||||||
|
padding_external
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let delete_button_resp = delete_column_button(ui, titlebar_resp, icon_width, padding_external);
|
||||||
|
let title_response = if delete_button_resp.clicked() {
|
||||||
|
Some(TitleResponse::RemoveColumn)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let titlebar_resp = TitleBarResponse {
|
||||||
|
title_response,
|
||||||
|
go_back: maybe_button_resp.map_or(false, |r| r.clicked()),
|
||||||
|
};
|
||||||
|
|
||||||
|
InnerResponse::new(titlebar_resp, delete_button_resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn back_button(ui: &mut egui::Ui, button_rect: egui::Rect) -> egui::Response {
|
||||||
|
let horizontal_length = 10.0;
|
||||||
|
let arrow_length = 5.0;
|
||||||
|
|
||||||
|
let helper = AnimationHelper::new_from_rect(ui, "note-compose-button", button_rect);
|
||||||
|
let painter = ui.painter_at(helper.get_animation_rect());
|
||||||
|
let stroke = Stroke::new(1.5, ui.visuals().text_color());
|
||||||
|
|
||||||
|
// Horizontal segment
|
||||||
|
let left_horizontal_point = pos2(-horizontal_length / 2., 0.);
|
||||||
|
let right_horizontal_point = pos2(horizontal_length / 2., 0.);
|
||||||
|
let scaled_left_horizontal_point = helper.scale_pos_from_center(left_horizontal_point);
|
||||||
|
let scaled_right_horizontal_point = helper.scale_pos_from_center(right_horizontal_point);
|
||||||
|
|
||||||
|
painter.line_segment(
|
||||||
|
[scaled_left_horizontal_point, scaled_right_horizontal_point],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Top Arrow
|
||||||
|
let sqrt_2_over_2 = std::f32::consts::SQRT_2 / 2.;
|
||||||
|
let right_top_arrow_point = helper.scale_pos_from_center(pos2(
|
||||||
|
left_horizontal_point.x + (sqrt_2_over_2 * arrow_length),
|
||||||
|
right_horizontal_point.y + sqrt_2_over_2 * arrow_length,
|
||||||
|
));
|
||||||
|
|
||||||
|
let scaled_left_arrow_point = scaled_left_horizontal_point;
|
||||||
|
painter.line_segment([scaled_left_arrow_point, right_top_arrow_point], stroke);
|
||||||
|
|
||||||
|
let right_bottom_arrow_point = helper.scale_pos_from_center(pos2(
|
||||||
|
left_horizontal_point.x + (sqrt_2_over_2 * arrow_length),
|
||||||
|
right_horizontal_point.y - sqrt_2_over_2 * arrow_length,
|
||||||
|
));
|
||||||
|
|
||||||
|
painter.line_segment([scaled_left_arrow_point, right_bottom_arrow_point], stroke);
|
||||||
|
|
||||||
|
helper.take_animation_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_column_button(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
allocation_response: egui::Response,
|
||||||
|
icon_width: f32,
|
||||||
|
padding: f32,
|
||||||
|
) -> egui::Response {
|
||||||
|
let img_size = 16.0;
|
||||||
|
let max_size = icon_width * ICON_EXPANSION_MULTIPLE;
|
||||||
|
|
||||||
|
let img_data = egui::include_image!("../assets/icons/column_delete_icon_4x.png");
|
||||||
|
let img = egui::Image::new(img_data).max_width(img_size);
|
||||||
|
|
||||||
|
let button_rect = {
|
||||||
|
let titlebar_rect = allocation_response.rect;
|
||||||
|
let titlebar_width = titlebar_rect.width();
|
||||||
|
let titlebar_center = titlebar_rect.center();
|
||||||
|
let button_center_y = titlebar_center.y;
|
||||||
|
let button_center_x =
|
||||||
|
titlebar_center.x + (titlebar_width / 2.0) - (max_size / 2.0) - padding;
|
||||||
|
egui::Rect::from_center_size(
|
||||||
|
pos2(button_center_x, button_center_y),
|
||||||
|
egui::vec2(max_size, max_size),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let helper = AnimationHelper::new_from_rect(ui, "delete-column-button", button_rect);
|
||||||
|
|
||||||
|
let cur_img_size = helper.scale_1d_pos(img_size);
|
||||||
|
|
||||||
|
let animation_rect = helper.get_animation_rect();
|
||||||
|
let animation_resp = helper.take_animation_response();
|
||||||
|
if allocation_response.union(animation_resp.clone()).hovered() {
|
||||||
|
img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
animation_resp
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
title_name: String,
|
||||||
|
titlebar_rect: egui::Rect,
|
||||||
|
icon_width: f32,
|
||||||
|
padding: f32,
|
||||||
|
) {
|
||||||
|
let painter = ui.painter_at(titlebar_rect);
|
||||||
|
|
||||||
|
let font = egui::FontId::new(
|
||||||
|
get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let max_title_width = titlebar_rect.width() - icon_width - padding * 2.;
|
||||||
|
let title_galley =
|
||||||
|
ui.fonts(|f| f.layout(title_name, font, ui.visuals().text_color(), max_title_width));
|
||||||
|
|
||||||
|
let pos = {
|
||||||
|
let titlebar_center = titlebar_rect.center();
|
||||||
|
let text_height = title_galley.rect.height();
|
||||||
|
|
||||||
|
let galley_pos_x = titlebar_rect.left() + padding;
|
||||||
|
let galley_pos_y = titlebar_center.y - (text_height / 2.);
|
||||||
|
pos2(galley_pos_x, galley_pos_y)
|
||||||
|
};
|
||||||
|
|
||||||
|
painter.galley(pos, title_galley, Color32::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TitleResponse {
|
||||||
|
RemoveColumn,
|
||||||
|
}
|
||||||
|
|||||||
78
src/route.rs
78
src/route.rs
@@ -1,9 +1,12 @@
|
|||||||
use enostr::NoteId;
|
use enostr::NoteId;
|
||||||
|
use nostrdb::Ndb;
|
||||||
use std::fmt::{self};
|
use std::fmt::{self};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
account_manager::AccountsRoute,
|
account_manager::AccountsRoute,
|
||||||
|
column::Columns,
|
||||||
timeline::{TimelineId, TimelineRoute},
|
timeline::{TimelineId, TimelineRoute},
|
||||||
|
ui::profile::preview::get_note_users_displayname_string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// App routing. These describe different places you can go inside Notedeck.
|
/// App routing. These describe different places you can go inside Notedeck.
|
||||||
@@ -13,6 +16,19 @@ pub enum Route {
|
|||||||
Accounts(AccountsRoute),
|
Accounts(AccountsRoute),
|
||||||
Relays,
|
Relays,
|
||||||
ComposeNote,
|
ComposeNote,
|
||||||
|
AddColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TitledRoute {
|
||||||
|
pub route: Route,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TitledRoute {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Route {
|
impl Route {
|
||||||
@@ -51,6 +67,42 @@ impl Route {
|
|||||||
pub fn add_account() -> Self {
|
pub fn add_account() -> Self {
|
||||||
Route::Accounts(AccountsRoute::AddAccount)
|
Route::Accounts(AccountsRoute::AddAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_titled_route(&self, columns: &Columns, ndb: &Ndb) -> TitledRoute {
|
||||||
|
let title = match self {
|
||||||
|
Route::Timeline(tlr) => match tlr {
|
||||||
|
TimelineRoute::Timeline(id) => {
|
||||||
|
let timeline = columns
|
||||||
|
.find_timeline(*id)
|
||||||
|
.expect("expected to find timeline");
|
||||||
|
timeline.kind.to_title(ndb)
|
||||||
|
}
|
||||||
|
TimelineRoute::Thread(id) => {
|
||||||
|
format!("{}'s Thread", get_note_users_displayname_string(ndb, id))
|
||||||
|
}
|
||||||
|
TimelineRoute::Reply(id) => {
|
||||||
|
format!("{}'s Reply", get_note_users_displayname_string(ndb, id))
|
||||||
|
}
|
||||||
|
TimelineRoute::Quote(id) => {
|
||||||
|
format!("{}'s Quote", get_note_users_displayname_string(ndb, id))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Route::Relays => "Relays".to_owned(),
|
||||||
|
|
||||||
|
Route::Accounts(amr) => match amr {
|
||||||
|
AccountsRoute::Accounts => "Accounts".to_owned(),
|
||||||
|
AccountsRoute::AddAccount => "Add Account".to_owned(),
|
||||||
|
},
|
||||||
|
Route::ComposeNote => "Compose Note".to_owned(),
|
||||||
|
Route::AddColumn => "Add Column".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
TitledRoute {
|
||||||
|
title,
|
||||||
|
route: *self,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add this to egui-nav so we don't have to deal with returning
|
// TODO: add this to egui-nav so we don't have to deal with returning
|
||||||
@@ -60,6 +112,7 @@ pub struct Router<R: Clone> {
|
|||||||
routes: Vec<R>,
|
routes: Vec<R>,
|
||||||
pub returning: bool,
|
pub returning: bool,
|
||||||
pub navigating: bool,
|
pub navigating: bool,
|
||||||
|
replacing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Clone> Router<R> {
|
impl<R: Clone> Router<R> {
|
||||||
@@ -69,10 +122,12 @@ impl<R: Clone> Router<R> {
|
|||||||
}
|
}
|
||||||
let returning = false;
|
let returning = false;
|
||||||
let navigating = false;
|
let navigating = false;
|
||||||
|
let replacing = false;
|
||||||
Router {
|
Router {
|
||||||
routes,
|
routes,
|
||||||
returning,
|
returning,
|
||||||
navigating,
|
navigating,
|
||||||
|
replacing,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +136,13 @@ impl<R: Clone> Router<R> {
|
|||||||
self.routes.push(route);
|
self.routes.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route to R. Then when it is successfully placed, should call `remove_previous_route`
|
||||||
|
pub fn route_to_replaced(&mut self, route: R) {
|
||||||
|
self.navigating = true;
|
||||||
|
self.replacing = true;
|
||||||
|
self.routes.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
/// Go back, start the returning process
|
/// Go back, start the returning process
|
||||||
pub fn go_back(&mut self) -> Option<R> {
|
pub fn go_back(&mut self) -> Option<R> {
|
||||||
if self.returning || self.routes.len() == 1 {
|
if self.returning || self.routes.len() == 1 {
|
||||||
@@ -99,6 +161,20 @@ impl<R: Clone> Router<R> {
|
|||||||
self.routes.pop()
|
self.routes.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_previous_route(&mut self) -> Option<R> {
|
||||||
|
let num_routes = self.routes.len();
|
||||||
|
if num_routes <= 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.returning = false;
|
||||||
|
self.replacing = false;
|
||||||
|
Some(self.routes.remove(num_routes - 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_replacing(&self) -> bool {
|
||||||
|
self.replacing
|
||||||
|
}
|
||||||
|
|
||||||
pub fn top(&self) -> &R {
|
pub fn top(&self) -> &R {
|
||||||
self.routes.last().expect("routes can't be empty")
|
self.routes.last().expect("routes can't be empty")
|
||||||
}
|
}
|
||||||
@@ -125,6 +201,8 @@ impl fmt::Display for Route {
|
|||||||
AccountsRoute::AddAccount => write!(f, "Add Account"),
|
AccountsRoute::AddAccount => write!(f, "Add Account"),
|
||||||
},
|
},
|
||||||
Route::ComposeNote => write!(f, "Compose Note"),
|
Route::ComposeNote => write!(f, "Compose Note"),
|
||||||
|
|
||||||
|
Route::AddColumn => write!(f, "Add Column"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::error::{Error, FilterError};
|
|||||||
use crate::filter;
|
use crate::filter;
|
||||||
use crate::filter::FilterState;
|
use crate::filter::FilterState;
|
||||||
use crate::timeline::Timeline;
|
use crate::timeline::Timeline;
|
||||||
|
use crate::ui::profile::preview::get_profile_displayname_string;
|
||||||
use enostr::{Filter, Pubkey};
|
use enostr::{Filter, Pubkey};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -136,7 +137,7 @@ impl TimelineKind {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
match Timeline::contact_list(&results[0].note) {
|
match Timeline::contact_list(&results[0].note, pk_src.clone()) {
|
||||||
Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new(
|
Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new(
|
||||||
TimelineKind::contact_list(pk_src),
|
TimelineKind::contact_list(pk_src),
|
||||||
FilterState::needs_remote(vec![contact_filter]),
|
FilterState::needs_remote(vec![contact_filter]),
|
||||||
@@ -150,4 +151,32 @@ impl TimelineKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_title(&self, ndb: &Ndb) -> String {
|
||||||
|
match self {
|
||||||
|
TimelineKind::List(list_kind) => match list_kind {
|
||||||
|
ListKind::Contact(pubkey_source) => match pubkey_source {
|
||||||
|
PubkeySource::Explicit(pubkey) => {
|
||||||
|
format!("{}'s Contacts", get_profile_displayname_string(ndb, pubkey))
|
||||||
|
}
|
||||||
|
PubkeySource::DeckAuthor => "Contacts".to_owned(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TimelineKind::Notifications(pubkey_source) => match pubkey_source {
|
||||||
|
PubkeySource::DeckAuthor => "Notifications".to_owned(),
|
||||||
|
PubkeySource::Explicit(pk) => format!(
|
||||||
|
"{}'s Notifications",
|
||||||
|
get_profile_displayname_string(ndb, pk)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
TimelineKind::Profile(pubkey_source) => match pubkey_source {
|
||||||
|
PubkeySource::DeckAuthor => "Profile".to_owned(),
|
||||||
|
PubkeySource::Explicit(pk) => {
|
||||||
|
format!("{}'s Profile", get_profile_displayname_string(ndb, pk))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TimelineKind::Universe => "Universe".to_owned(),
|
||||||
|
TimelineKind::Generic => "Custom Filter".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use std::fmt;
|
|||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
use enostr::Pubkey;
|
|
||||||
use nostrdb::{Ndb, Note, Subscription, Transaction};
|
use nostrdb::{Ndb, Note, Subscription, Transaction};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
@@ -180,9 +179,8 @@ pub struct Timeline {
|
|||||||
|
|
||||||
impl Timeline {
|
impl Timeline {
|
||||||
/// Create a timeline from a contact list
|
/// Create a timeline from a contact list
|
||||||
pub fn contact_list(contact_list: &Note) -> Result<Self> {
|
pub fn contact_list(contact_list: &Note, pk_src: PubkeySource) -> Result<Self> {
|
||||||
let filter = filter::filter_from_tags(contact_list)?.into_follow_filter();
|
let filter = filter::filter_from_tags(contact_list)?.into_follow_filter();
|
||||||
let pk_src = PubkeySource::Explicit(Pubkey::new(*contact_list.pubkey()));
|
|
||||||
|
|
||||||
Ok(Timeline::new(
|
Ok(Timeline::new(
|
||||||
TimelineKind::contact_list(pk_src),
|
TimelineKind::contact_list(pk_src),
|
||||||
@@ -241,13 +239,15 @@ impl Timeline {
|
|||||||
|
|
||||||
pub fn poll_notes_into_view(
|
pub fn poll_notes_into_view(
|
||||||
timeline_idx: usize,
|
timeline_idx: usize,
|
||||||
timelines: &mut [Timeline],
|
mut timelines: Vec<&mut Timeline>,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
unknown_ids: &mut UnknownIds,
|
unknown_ids: &mut UnknownIds,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let timeline = &mut timelines[timeline_idx];
|
let timeline = timelines
|
||||||
|
.get_mut(timeline_idx)
|
||||||
|
.ok_or(Error::TimelineNotFound)?;
|
||||||
let sub = timeline.subscription.ok_or(Error::no_active_sub())?;
|
let sub = timeline.subscription.ok_or(Error::no_active_sub())?;
|
||||||
|
|
||||||
let new_note_ids = ndb.poll_for_notes(sub, 500);
|
let new_note_ids = ndb.poll_for_notes(sub, 500);
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ pub enum TimelineRoute {
|
|||||||
Quote(NoteId),
|
Quote(NoteId),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum TimelineRouteResponse {
|
pub enum AfterRouteExecution {
|
||||||
Post(PostResponse),
|
Post(PostResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimelineRouteResponse {
|
impl AfterRouteExecution {
|
||||||
pub fn post(post: PostResponse) -> Self {
|
pub fn post(post: PostResponse) -> Self {
|
||||||
TimelineRouteResponse::Post(post)
|
AfterRouteExecution::Post(post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ pub fn render_timeline_route(
|
|||||||
col: usize,
|
col: usize,
|
||||||
textmode: bool,
|
textmode: bool,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
) -> Option<TimelineRouteResponse> {
|
) -> Option<AfterRouteExecution> {
|
||||||
match route {
|
match route {
|
||||||
TimelineRoute::Timeline(timeline_id) => {
|
TimelineRoute::Timeline(timeline_id) => {
|
||||||
if let Some(bar_action) =
|
if let Some(bar_action) =
|
||||||
@@ -58,7 +58,8 @@ pub fn render_timeline_route(
|
|||||||
.ui(ui)
|
.ui(ui)
|
||||||
{
|
{
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
let router = columns.columns_mut()[col].router_mut();
|
let mut cur_column = columns.columns_mut();
|
||||||
|
let router = cur_column[col].router_mut();
|
||||||
|
|
||||||
bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn);
|
bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn);
|
||||||
}
|
}
|
||||||
@@ -73,7 +74,8 @@ pub fn render_timeline_route(
|
|||||||
.ui(ui)
|
.ui(ui)
|
||||||
{
|
{
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
let router = columns.columns_mut()[col].router_mut();
|
let mut cur_column = columns.columns_mut();
|
||||||
|
let router = cur_column[col].router_mut();
|
||||||
bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn);
|
bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ pub fn render_timeline_route(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TimelineRouteResponse::post(response.inner))
|
Some(AfterRouteExecution::post(response.inner))
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineRoute::Quote(id) => {
|
TimelineRoute::Quote(id) => {
|
||||||
@@ -140,7 +142,7 @@ pub fn render_timeline_route(
|
|||||||
np.to_quote(seckey, ¬e)
|
np.to_quote(seckey, ¬e)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(TimelineRouteResponse::post(response.inner))
|
Some(AfterRouteExecution::post(response.inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
246
src/ui/add_column.rs
Normal file
246
src/ui/add_column.rs
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
use egui::{pos2, vec2, Color32, FontId, ImageSource, Pos2, Rect, Separator, Ui};
|
||||||
|
use nostrdb::Ndb;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_style::{get_font_size, NotedeckTextStyle},
|
||||||
|
timeline::{PubkeySource, Timeline, TimelineKind},
|
||||||
|
ui::anim::ICON_EXPANSION_MULTIPLE,
|
||||||
|
user_account::UserAccount,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::anim::AnimationHelper;
|
||||||
|
|
||||||
|
pub enum AddColumnResponse {
|
||||||
|
Timeline(Timeline),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum AddColumnOption {
|
||||||
|
Universe,
|
||||||
|
Notification(PubkeySource),
|
||||||
|
Home(PubkeySource),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddColumnOption {
|
||||||
|
pub fn take_as_response(
|
||||||
|
self,
|
||||||
|
ndb: &Ndb,
|
||||||
|
cur_account: Option<&UserAccount>,
|
||||||
|
) -> Option<AddColumnResponse> {
|
||||||
|
match self {
|
||||||
|
AddColumnOption::Universe => TimelineKind::Universe
|
||||||
|
.into_timeline(ndb, None)
|
||||||
|
.map(AddColumnResponse::Timeline),
|
||||||
|
AddColumnOption::Notification(pubkey) => TimelineKind::Notifications(pubkey)
|
||||||
|
.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes()))
|
||||||
|
.map(AddColumnResponse::Timeline),
|
||||||
|
AddColumnOption::Home(pubkey) => {
|
||||||
|
let tlk = TimelineKind::contact_list(pubkey);
|
||||||
|
tlk.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes()))
|
||||||
|
.map(AddColumnResponse::Timeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AddColumnView<'a> {
|
||||||
|
ndb: &'a Ndb,
|
||||||
|
cur_account: Option<&'a UserAccount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AddColumnView<'a> {
|
||||||
|
pub fn new(ndb: &'a Ndb, cur_account: Option<&'a UserAccount>) -> Self {
|
||||||
|
Self { ndb, cur_account }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
|
||||||
|
let mut selected_option: Option<AddColumnResponse> = None;
|
||||||
|
for column_option_data in self.get_column_options() {
|
||||||
|
let option = column_option_data.option.clone();
|
||||||
|
if self.column_option_ui(ui, column_option_data).clicked() {
|
||||||
|
selected_option = option.take_as_response(self.ndb, self.cur_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add(Separator::default().spacing(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_option
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_option_ui(&mut self, ui: &mut Ui, data: ColumnOptionData) -> egui::Response {
|
||||||
|
let icon_padding = 8.0;
|
||||||
|
let min_icon_width = 32.0;
|
||||||
|
let height_padding = 12.0;
|
||||||
|
let max_width = ui.available_width();
|
||||||
|
let title_style = NotedeckTextStyle::Body;
|
||||||
|
let desc_style = NotedeckTextStyle::Button;
|
||||||
|
let title_min_font_size = get_font_size(ui.ctx(), &title_style);
|
||||||
|
let desc_min_font_size = get_font_size(ui.ctx(), &desc_style);
|
||||||
|
|
||||||
|
let max_height = {
|
||||||
|
let max_wrap_width =
|
||||||
|
max_width - ((icon_padding * 2.0) + (min_icon_width * ICON_EXPANSION_MULTIPLE));
|
||||||
|
let title_max_font = FontId::new(
|
||||||
|
title_min_font_size * ICON_EXPANSION_MULTIPLE,
|
||||||
|
title_style.font_family(),
|
||||||
|
);
|
||||||
|
let desc_max_font = FontId::new(
|
||||||
|
desc_min_font_size * ICON_EXPANSION_MULTIPLE,
|
||||||
|
desc_style.font_family(),
|
||||||
|
);
|
||||||
|
let max_desc_galley = ui.fonts(|f| {
|
||||||
|
f.layout(
|
||||||
|
data.description.to_string(),
|
||||||
|
desc_max_font,
|
||||||
|
Color32::WHITE,
|
||||||
|
max_wrap_width,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let max_title_galley = ui.fonts(|f| {
|
||||||
|
f.layout(
|
||||||
|
data.title.to_string(),
|
||||||
|
title_max_font,
|
||||||
|
Color32::WHITE,
|
||||||
|
max_wrap_width,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let desc_font_max_size = max_desc_galley.rect.height();
|
||||||
|
let title_font_max_size = max_title_galley.rect.height();
|
||||||
|
title_font_max_size + desc_font_max_size + (2.0 * height_padding)
|
||||||
|
};
|
||||||
|
|
||||||
|
let helper = AnimationHelper::new(ui, data.title, vec2(max_width, max_height));
|
||||||
|
let animation_rect = helper.get_animation_rect();
|
||||||
|
|
||||||
|
let cur_icon_width = helper.scale_1d_pos(min_icon_width);
|
||||||
|
let painter = ui.painter_at(animation_rect);
|
||||||
|
|
||||||
|
let cur_icon_size = vec2(cur_icon_width, cur_icon_width);
|
||||||
|
let cur_icon_x_pos = animation_rect.left() + (icon_padding) + (cur_icon_width / 2.0);
|
||||||
|
|
||||||
|
let title_cur_font = FontId::new(
|
||||||
|
helper.scale_1d_pos(title_min_font_size),
|
||||||
|
title_style.font_family(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let desc_cur_font = FontId::new(
|
||||||
|
helper.scale_1d_pos(desc_min_font_size),
|
||||||
|
desc_style.font_family(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let wrap_width = max_width - (cur_icon_width + (icon_padding * 2.0));
|
||||||
|
let text_color = ui.ctx().style().visuals.text_color();
|
||||||
|
let fallback_color = ui.ctx().style().visuals.weak_text_color();
|
||||||
|
|
||||||
|
let title_galley = painter.layout(
|
||||||
|
data.title.to_string(),
|
||||||
|
title_cur_font,
|
||||||
|
text_color,
|
||||||
|
wrap_width,
|
||||||
|
);
|
||||||
|
let desc_galley = painter.layout(
|
||||||
|
data.description.to_string(),
|
||||||
|
desc_cur_font,
|
||||||
|
text_color,
|
||||||
|
wrap_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
let galley_heights = title_galley.rect.height() + desc_galley.rect.height();
|
||||||
|
|
||||||
|
let cur_height_padding = (animation_rect.height() - galley_heights) / 2.0;
|
||||||
|
let corner_x_pos = cur_icon_x_pos + (cur_icon_width / 2.0) + icon_padding;
|
||||||
|
let title_corner_pos = Pos2::new(corner_x_pos, animation_rect.top() + cur_height_padding);
|
||||||
|
let desc_corner_pos = Pos2::new(
|
||||||
|
corner_x_pos,
|
||||||
|
title_corner_pos.y + title_galley.rect.height(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let icon_cur_y = animation_rect.top() + cur_height_padding + (galley_heights / 2.0);
|
||||||
|
let icon_img = egui::Image::new(data.icon).fit_to_exact_size(cur_icon_size);
|
||||||
|
let icon_rect = Rect::from_center_size(pos2(cur_icon_x_pos, icon_cur_y), cur_icon_size);
|
||||||
|
|
||||||
|
icon_img.paint_at(ui, icon_rect);
|
||||||
|
painter.galley(title_corner_pos, title_galley, fallback_color);
|
||||||
|
painter.galley(desc_corner_pos, desc_galley, fallback_color);
|
||||||
|
|
||||||
|
helper.take_animation_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_column_options(&self) -> Vec<ColumnOptionData> {
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
vec.push(ColumnOptionData {
|
||||||
|
title: "Universe",
|
||||||
|
description: "See the whole nostr universe",
|
||||||
|
icon: egui::include_image!("../../assets/icons/universe_icon_dark_4x.png"),
|
||||||
|
option: AddColumnOption::Universe,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(acc) = self.cur_account {
|
||||||
|
let source = if acc.secret_key.is_some() {
|
||||||
|
PubkeySource::DeckAuthor
|
||||||
|
} else {
|
||||||
|
PubkeySource::Explicit(acc.pubkey)
|
||||||
|
};
|
||||||
|
|
||||||
|
vec.push(ColumnOptionData {
|
||||||
|
title: "Home timeline",
|
||||||
|
description: "See recommended notes first",
|
||||||
|
icon: egui::include_image!("../../assets/icons/home_icon_dark_4x.png"),
|
||||||
|
option: AddColumnOption::Home(source.clone()),
|
||||||
|
});
|
||||||
|
vec.push(ColumnOptionData {
|
||||||
|
title: "Notifications",
|
||||||
|
description: "Stay up to date with notifications and mentions",
|
||||||
|
icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
|
||||||
|
option: AddColumnOption::Notification(source),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ColumnOptionData {
|
||||||
|
title: &'static str,
|
||||||
|
description: &'static str,
|
||||||
|
icon: ImageSource<'static>,
|
||||||
|
option: AddColumnOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod preview {
|
||||||
|
use crate::{
|
||||||
|
test_data,
|
||||||
|
ui::{Preview, PreviewConfig, View},
|
||||||
|
Damus,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::AddColumnView;
|
||||||
|
|
||||||
|
pub struct AddColumnPreview {
|
||||||
|
app: Damus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddColumnPreview {
|
||||||
|
fn new() -> Self {
|
||||||
|
let app = test_data::test_app();
|
||||||
|
|
||||||
|
AddColumnPreview { app }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for AddColumnPreview {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
AddColumnView::new(&self.app.ndb, self.app.accounts.get_selected_account()).ui(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Preview for AddColumnView<'a> {
|
||||||
|
type Prev = AddColumnPreview;
|
||||||
|
|
||||||
|
fn preview(_cfg: PreviewConfig) -> Self::Prev {
|
||||||
|
AddColumnPreview::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,27 @@ impl AnimationHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_from_rect(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
animation_name: impl std::hash::Hash,
|
||||||
|
animation_rect: egui::Rect,
|
||||||
|
) -> Self {
|
||||||
|
let id = ui.id().with(animation_name);
|
||||||
|
let response = ui.allocate_rect(animation_rect, Sense::click());
|
||||||
|
|
||||||
|
let animation_progress =
|
||||||
|
ui.ctx()
|
||||||
|
.animate_bool_with_time(id, response.hovered(), ANIM_SPEED);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rect: animation_rect,
|
||||||
|
center: animation_rect.center(),
|
||||||
|
response,
|
||||||
|
animation_progress,
|
||||||
|
expansion_multiple: ICON_EXPANSION_MULTIPLE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scale_1d_pos(&self, min_object_size: f32) -> f32 {
|
pub fn scale_1d_pos(&self, min_object_size: f32) -> f32 {
|
||||||
let max_object_size = min_object_size * self.expansion_multiple;
|
let max_object_size = min_object_size * self.expansion_multiple;
|
||||||
|
|
||||||
@@ -93,4 +114,8 @@ impl AnimationHelper {
|
|||||||
self.center.y + self.scale_1d_pos(y_min),
|
self.center.y + self.scale_1d_pos(y_min),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scale_pos_from_center(&self, min_pos: Pos2) -> Pos2 {
|
||||||
|
self.scale_from_center(min_pos.x, min_pos.y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod account_login_view;
|
pub mod account_login_view;
|
||||||
pub mod account_management;
|
pub mod account_management;
|
||||||
|
pub mod add_column;
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod mention;
|
pub mod mention;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::{colors, images, DisplayName};
|
|||||||
use egui::load::TexturePoll;
|
use egui::load::TexturePoll;
|
||||||
use egui::{Frame, RichText, Sense, Widget};
|
use egui::{Frame, RichText, Sense, Widget};
|
||||||
use egui_extras::Size;
|
use egui_extras::Size;
|
||||||
|
use enostr::NoteId;
|
||||||
use nostrdb::ProfileRecord;
|
use nostrdb::ProfileRecord;
|
||||||
|
|
||||||
pub struct ProfilePreview<'a, 'cache> {
|
pub struct ProfilePreview<'a, 'cache> {
|
||||||
@@ -256,3 +257,28 @@ fn about_section_widget<'a>(profile: &'a ProfileRecord<'a>) -> impl egui::Widget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_display_name_as_string(profile: Option<&'_ ProfileRecord<'_>>) -> String {
|
||||||
|
let display_name = get_display_name(profile);
|
||||||
|
match display_name {
|
||||||
|
DisplayName::One(n) => n.to_string(),
|
||||||
|
DisplayName::Both { display_name, .. } => display_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_profile_displayname_string(ndb: &nostrdb::Ndb, pk: &enostr::Pubkey) -> String {
|
||||||
|
let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked");
|
||||||
|
let profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok();
|
||||||
|
get_display_name_as_string(profile.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_note_users_displayname_string(ndb: &nostrdb::Ndb, id: &NoteId) -> String {
|
||||||
|
let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked");
|
||||||
|
let note = ndb.get_note_by_id(&txn, id.bytes());
|
||||||
|
let profile = if let Ok(note) = note {
|
||||||
|
ndb.get_profile_by_pubkey(&txn, note.pubkey()).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
get_display_name_as_string(profile.as_ref())
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ use tracing::info;
|
|||||||
use crate::{
|
use crate::{
|
||||||
account_manager::AccountsRoute,
|
account_manager::AccountsRoute,
|
||||||
colors,
|
colors,
|
||||||
column::Column,
|
column::{Column, Columns},
|
||||||
imgcache::ImageCache,
|
imgcache::ImageCache,
|
||||||
route::{Route, Router},
|
route::Route,
|
||||||
user_account::UserAccount,
|
user_account::UserAccount,
|
||||||
Damus,
|
Damus,
|
||||||
};
|
};
|
||||||
@@ -162,7 +162,8 @@ impl<'a> DesktopSidePanel<'a> {
|
|||||||
helper.take_animation_response()
|
helper.take_animation_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perform_action(router: &mut Router<Route>, action: SidePanelAction) {
|
pub fn perform_action(columns: &mut Columns, action: SidePanelAction) {
|
||||||
|
let router = columns.get_first_router();
|
||||||
match action {
|
match action {
|
||||||
SidePanelAction::Panel => {} // TODO
|
SidePanelAction::Panel => {} // TODO
|
||||||
SidePanelAction::Account => {
|
SidePanelAction::Account => {
|
||||||
@@ -186,8 +187,11 @@ impl<'a> DesktopSidePanel<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SidePanelAction::Columns => {
|
SidePanelAction::Columns => {
|
||||||
// TODO
|
if router.routes().iter().any(|&r| r == Route::AddColumn) {
|
||||||
info!("Clicked columns button");
|
router.go_back();
|
||||||
|
} else {
|
||||||
|
columns.new_column_picker();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SidePanelAction::ComposeNote => {
|
SidePanelAction::ComposeNote => {
|
||||||
if router.routes().iter().any(|&r| r == Route::ComposeNote) {
|
if router.routes().iter().any(|&r| r == Route::ComposeNote) {
|
||||||
@@ -366,9 +370,7 @@ mod preview {
|
|||||||
impl DesktopSidePanelPreview {
|
impl DesktopSidePanelPreview {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let mut app = test_data::test_app();
|
let mut app = test_data::test_app();
|
||||||
app.columns
|
app.columns.add_column(Column::new(vec![Route::accounts()]));
|
||||||
.columns_mut()
|
|
||||||
.push(Column::new(vec![Route::accounts()]));
|
|
||||||
DesktopSidePanelPreview { app }
|
DesktopSidePanelPreview { app }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,10 +390,7 @@ mod preview {
|
|||||||
);
|
);
|
||||||
let response = panel.show(ui);
|
let response = panel.show(ui);
|
||||||
|
|
||||||
DesktopSidePanel::perform_action(
|
DesktopSidePanel::perform_action(&mut self.app.columns, response.action);
|
||||||
self.app.columns.columns_mut()[0].router_mut(),
|
|
||||||
response.action,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use notedeck::app_creation::{
|
use notedeck::app_creation::{
|
||||||
generate_mobile_emulator_native_options, generate_native_options, setup_cc,
|
generate_mobile_emulator_native_options, generate_native_options, setup_cc,
|
||||||
};
|
};
|
||||||
|
use notedeck::ui::add_column::AddColumnView;
|
||||||
use notedeck::ui::{
|
use notedeck::ui::{
|
||||||
account_login_view::AccountLoginView, account_management::AccountsView, DesktopSidePanel,
|
account_login_view::AccountLoginView, account_management::AccountsView, DesktopSidePanel,
|
||||||
PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, RelayView,
|
PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, RelayView,
|
||||||
@@ -102,5 +103,6 @@ async fn main() {
|
|||||||
AccountsView,
|
AccountsView,
|
||||||
DesktopSidePanel,
|
DesktopSidePanel,
|
||||||
PostView,
|
PostView,
|
||||||
|
AddColumnView,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user