Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jprochazk committed Dec 15, 2024
1 parent 596706c commit abd712b
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 44 deletions.
26 changes: 26 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3666,6 +3666,31 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"

[[package]]
name = "jiff"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db69f08d4fb10524cacdb074c10b296299d71274ddbc830a8ee65666867002e9"
dependencies = [
"jiff-tzdb-platform",
"windows-sys 0.59.0",
]

[[package]]
name = "jiff-tzdb"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653"

[[package]]
name = "jiff-tzdb-platform"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329"
dependencies = [
"jiff-tzdb",
]

[[package]]
name = "jni"
version = "0.21.1"
Expand Down Expand Up @@ -6481,6 +6506,7 @@ dependencies = [
"egui_extras",
"egui_kittest",
"egui_tiles",
"jiff",
"once_cell",
"parking_lot",
"rand",
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ indicatif = "0.17.7" # Progress bar
infer = "0.16" # infer MIME type by checking the magic number signaturefer MIME type by checking the magic number signature
insta = "1.23"
itertools = "0.13"
jiff = "0.1.15"
js-sys = "0.3"
libc = "0.2"
linked-hash-map = { version = "0.5", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions crates/viewer/re_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ smallvec.workspace = true
strum_macros.workspace = true
strum.workspace = true
sublime_fuzzy.workspace = true
jiff.workspace = true


[dev-dependencies]
Expand Down
215 changes: 171 additions & 44 deletions crates/viewer/re_ui/src/notifications.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::time::Duration;

use egui::hex_color;
use jiff::{Unit, Zoned};
pub use re_log::Level;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -30,7 +32,9 @@ fn is_relevant(level: re_log::Level) -> bool {
struct Notification {
level: NotificationLevel,
text: String,
ttl_sec: f64,

created_at: Zoned,
ttl: Duration,
}

pub struct NotificationUi {
Expand Down Expand Up @@ -69,32 +73,40 @@ impl NotificationUi {
self.data.push(Notification {
level: level.into(),
text: text.into(),
ttl_sec: TOAST_TTL_SEC,

created_at: Zoned::now(),
ttl: base_ttl(),
});
}

pub fn success(&mut self, text: impl Into<String>) {
self.data.push(Notification {
level: NotificationLevel::Success,
text: text.into(),
ttl_sec: TOAST_TTL_SEC,

created_at: Zoned::now(),
ttl: base_ttl(),
});
}

pub fn show(&mut self, egui_ctx: &egui::Context) {
self.panel.show(egui_ctx, &self.data[..]);

if self.panel.is_visible {
for notification in &mut self.data {
notification.ttl_sec = 0.0;
notification.ttl = Duration::ZERO;
}
}

self.toasts.show(egui_ctx, &mut self.data[..]);
let mut to_dismiss = None;
self.panel.show(egui_ctx, &self.data[..], &mut to_dismiss);
if let Some(i) = to_dismiss {
self.data.remove(i);
}

self.toasts.show(egui_ctx, &mut self.data[..]);

if let Some(notification) = self.data.last() {
if notification.ttl_sec.is_finite() && notification.ttl_sec > 0.0 {
egui_ctx.request_repaint_after(Duration::from_secs_f64(notification.ttl_sec));
if !notification.ttl.is_zero() {
egui_ctx.request_repaint_after(notification.ttl);
}
}
}
Expand All @@ -113,21 +125,50 @@ impl NotificationPanel {
}
}

fn show(&self, egui_ctx: &egui::Context, notifications: &[Notification]) {
fn show(
&self,
egui_ctx: &egui::Context,
notifications: &[Notification],
to_dismiss: &mut Option<usize>,
) {
if !self.is_visible {
return;
}

let panel_width = 400.0;
let panel_width = 358.0;
let panel_max_height = 320.0;

let notification_list = |ui: &mut egui::Ui| {
if notifications.is_empty() {
ui.label(
egui::RichText::new("Nothing here!")
.weak()
.color(hex_color!("#636b6f")),
);

return;
}

for (i, notification) in notifications.iter().enumerate().rev() {
if let Some(action) = show_notification(ui, notification, DisplayMode::Panel).action
{
match action {
Action::Dismiss => {
*to_dismiss = Some(i);
}
}
};
}
};

egui::Area::new(self.id)
.anchor(egui::Align2::RIGHT_TOP, egui::vec2(-8.0, 32.0))
.order(egui::Order::Foreground)
.interactable(true)
.movable(false)
.show(egui_ctx, |ui| {
egui::Frame::window(ui.style())
.fill(hex_color!("#141819"))
.rounding(0.0)
.show(ui, |ui| {
ui.set_width(panel_width);
Expand All @@ -137,23 +178,15 @@ impl NotificationPanel {
egui::scroll_area::ScrollBarVisibility::AlwaysVisible,
)
.min_scrolled_height(panel_max_height)
.show(ui, |ui| {
for Notification { level, text, .. } in notifications.iter().rev() {
ui.horizontal(|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
ui.set_max_width(panel_width);
ui.spacing_mut().item_spacing = egui::Vec2::splat(5.0);
log_level_icon(ui, *level);
ui.label(format!("{level:?}: {text}"));
});
}
});
.show(ui, notification_list);
});
});
}
}

const TOAST_TTL_SEC: f64 = 4.0;
fn base_ttl() -> Duration {
Duration::from_secs_f64(4.0)
}

struct Toasts {
id: egui::Id,
Expand All @@ -176,12 +209,12 @@ impl Toasts {
fn show(&mut self, egui_ctx: &egui::Context, notifications: &mut [Notification]) {
let Self { id } = self;

let dt = egui_ctx.input(|i| i.unstable_dt) as f64;
let mut offset = egui::vec2(-8.0, 8.0);
let dt = Duration::from_secs_f32(egui_ctx.input(|i| i.unstable_dt));
let mut offset = egui::vec2(-8.0, 32.0);

for (i, notification) in notifications
.iter_mut()
.filter(|n| n.ttl_sec > 0.0)
.filter(|n| n.ttl > Duration::ZERO)
.enumerate()
{
let response = egui::Area::new(id.with(i))
Expand All @@ -190,47 +223,141 @@ impl Toasts {
.interactable(true)
.movable(false)
.show(egui_ctx, |ui| {
show_notification_toast(ui, notification);
show_notification(ui, notification, DisplayMode::Toast).response
})
.response;

if !response.hovered() {
notification.ttl_sec = (notification.ttl_sec - dt).max(0.0);
if notification.ttl < dt {
notification.ttl = Duration::ZERO;
} else {
notification.ttl -= dt;
}
}

let response = response.on_hover_text("Click to close and copy contents");

if response.clicked() {
egui_ctx.output_mut(|o| o.copied_text = notification.text.clone());
notification.ttl_sec = 0.0;
notification.ttl = Duration::ZERO;
}

offset.y += response.rect.height() + 8.0;
}
}
}

fn show_notification_toast(ui: &mut egui::Ui, notification: &Notification) -> egui::Response {
egui::Frame::window(ui.style())
#[derive(Clone, Copy, PartialEq, Eq)]
enum DisplayMode {
Panel,
Toast,
}

#[derive(Debug, Clone, Copy)]
enum Action {
Dismiss,
}

struct NotificationResponse {
response: egui::Response,
action: Option<Action>,
}

fn show_notification(
ui: &mut egui::Ui,
notification: &Notification,
mode: DisplayMode,
) -> NotificationResponse {
let mut action = None;

let response = egui::Frame::window(ui.style())
.rounding(4.0)
.inner_margin(10.0)
.fill(hex_color!("#1c2123"))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
ui.set_max_width(400.0);
ui.spacing_mut().item_spacing = egui::Vec2::splat(5.0);
log_level_icon(ui, notification.level);
ui.label(notification.text.clone());
ui.vertical_centered(|ui| {
let text_response = ui
.horizontal_top(|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
ui.set_max_width(300.0);
ui.spacing_mut().item_spacing.x = 8.0;
log_level_icon(ui, notification.level);
ui.label(
egui::RichText::new(notification.text.clone())
.color(hex_color!("#cad8de"))
.weak(),
);

ui.spacing_mut().item_spacing.x = 4.0;
if mode == DisplayMode::Panel {
notification_age_label(ui, notification);
}
})
.response;

let controls_response = ui
.horizontal_top(|ui| {
if mode != DisplayMode::Panel {
return;
}

ui.add_space(17.0);
if ui.button("Dismiss").clicked() {
action = Some(Action::Dismiss);
}
})
.response;

text_response.union(controls_response)
})
})
.response
.response;

NotificationResponse { response, action }
}

fn notification_age_label(ui: &mut egui::Ui, notification: &Notification) {
let Ok(age) = (&Zoned::now() - &notification.created_at).total(Unit::Second) else {
return;
};

let formatted = if age <= 9.0 {
ui.ctx().request_repaint_after(Duration::from_secs(1));

"just now".to_owned()
} else if age <= 59.0 {
ui.ctx().request_repaint_after(Duration::from_secs(1));

format!("{age:.0}s")
} else {
ui.ctx().request_repaint_after(Duration::from_secs(60));

notification.created_at.time().strftime("%H:%M").to_string()
};

ui.horizontal_top(|ui| {
ui.set_min_width(52.0);
ui.with_layout(egui::Layout::top_down(egui::Align::Max), |ui| {
ui.label(
egui::RichText::new(formatted)
.weak()
.color(hex_color!("#636b6f")),
)
.on_hover_text(format!("{}", notification.created_at));
});
});
}

fn log_level_icon(ui: &mut egui::Ui, level: NotificationLevel) {
let (icon, icon_color) = match level {
NotificationLevel::Info => ("ℹ", crate::INFO_COLOR),
NotificationLevel::Warning => ("⚠", ui.style().visuals.warn_fg_color),
NotificationLevel::Error => ("❗", ui.style().visuals.error_fg_color),
NotificationLevel::Success => ("✔", crate::SUCCESS_COLOR),
let color = match level {
NotificationLevel::Info => crate::INFO_COLOR,
NotificationLevel::Warning => ui.style().visuals.warn_fg_color,
NotificationLevel::Error => ui.style().visuals.error_fg_color,
NotificationLevel::Success => crate::SUCCESS_COLOR,
};
ui.label(egui::RichText::new(icon).color(icon_color));

let (rect, _) = ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover());
let mut pos = rect.center();
pos.y += 2.0;
ui.painter().circle_filled(pos, 5.0, color);
}

0 comments on commit abd712b

Please sign in to comment.