Files
notedeck/crates/notedeck_chrome/src/chrome.rs
William Casarin 3d4db820b4 i18n: make localization context non-global
- Simplify Localization{Context,Manager} to just Localization
- Fixed a bunch of lifetime issueo
- Removed all Arcs and Locks
- Removed globals
  * widgets now need access to &mut Localization for i18n

Signed-off-by: William Casarin <jb55@jb55.com>
2025-07-22 09:49:32 -07:00

904 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Entry point for wasm
//#[cfg(target_arch = "wasm32")]
//use wasm_bindgen::prelude::*;
use crate::app::NotedeckApp;
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction};
use notedeck::{
tr, App, AppAction, AppContext, Localization, NotedeckTextStyle, UserAccount, WalletType,
};
use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
};
use notedeck_dave::{Dave, DaveAvatar};
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
static ICON_WIDTH: f32 = 40.0;
pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
pub struct Chrome {
active: i32,
open: bool,
tab_selected: i32,
apps: Vec<NotedeckApp>,
#[cfg(feature = "memory")]
show_memory_debug: bool,
}
impl Default for Chrome {
fn default() -> Self {
Self {
active: 0,
tab_selected: 0,
// sidemenu is not open by default on mobile/narrow uis
open: !notedeck::ui::is_compiled_as_mobile(),
apps: vec![],
#[cfg(feature = "memory")]
show_memory_debug: false,
}
}
}
/// When you click the toolbar button, these actions
/// are returned
#[derive(Debug, Eq, PartialEq)]
pub enum ToolbarAction {
Notifications,
Dave,
Home,
}
pub enum ChromePanelAction {
Support,
Settings,
Account,
Wallet,
Toolbar(ToolbarAction),
SaveTheme(ThemePreference),
}
impl ChromePanelAction {
fn columns_switch(ctx: &mut AppContext, chrome: &mut Chrome, kind: &TimelineKind) {
chrome.switch_to_columns();
let Some(columns_app) = chrome.get_columns_app() else {
return;
};
if let Some(active_columns) = columns_app
.decks_cache
.active_columns_mut(ctx.i18n, ctx.accounts)
{
match active_columns.select_by_kind(kind) {
SelectionResult::NewSelection(_index) => {
// great! no need to go to top yet
}
SelectionResult::AlreadySelected(_n) => {
// we already selected this, so scroll to top
columns_app.scroll_to_top();
}
SelectionResult::Failed => {
// oh no, something went wrong
// TODO(jb55): handle tab selection failure
}
}
}
}
fn columns_navigate(ctx: &mut AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) {
chrome.switch_to_columns();
if let Some(c) = chrome.get_columns_app().and_then(|columns| {
columns
.decks_cache
.selected_column_mut(ctx.i18n, ctx.accounts)
}) {
if c.router().routes().iter().any(|r| r == &route) {
// return if we are already routing to accounts
c.router_mut().go_back();
} else {
c.router_mut().route_to(route);
//c..route_to(Route::relays());
}
};
}
fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
match self {
Self::SaveTheme(theme) => {
ui.ctx().options_mut(|o| {
o.theme_preference = *theme;
});
ctx.theme.save(*theme);
}
Self::Toolbar(toolbar_action) => match toolbar_action {
ToolbarAction::Dave => chrome.switch_to_dave(),
ToolbarAction::Home => {
Self::columns_switch(
ctx,
chrome,
&TimelineKind::List(ListKind::Contact(
ctx.accounts.get_selected_account().key.pubkey,
)),
);
}
ToolbarAction::Notifications => {
Self::columns_switch(
ctx,
chrome,
&TimelineKind::Notifications(
ctx.accounts.get_selected_account().key.pubkey,
),
);
}
},
Self::Support => {
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Support);
}
Self::Account => {
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::accounts());
}
Self::Settings => {
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Relays);
}
Self::Wallet => {
Self::columns_navigate(
ctx,
chrome,
notedeck_columns::Route::Wallet(WalletType::Auto),
);
}
}
}
}
impl Chrome {
pub fn new() -> Self {
Chrome::default()
}
pub fn toggle(&mut self) {
self.open = !self.open;
}
pub fn add_app(&mut self, app: NotedeckApp) {
self.apps.push(app);
}
fn get_columns_app(&mut self) -> Option<&mut Damus> {
for app in &mut self.apps {
if let NotedeckApp::Columns(cols) = app {
return Some(cols);
}
}
None
}
fn get_dave(&mut self) -> Option<&mut Dave> {
for app in &mut self.apps {
if let NotedeckApp::Dave(dave) = app {
return Some(dave);
}
}
None
}
fn switch_to_dave(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Dave(_) = app {
self.active = i as i32;
}
}
}
fn switch_to_columns(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Columns(_) = app {
self.active = i as i32;
}
}
}
pub fn set_active(&mut self, app: i32) {
self.active = app;
}
/// The chrome side panel
fn panel(
&mut self,
app_ctx: &mut AppContext,
builder: StripBuilder,
amt_open: f32,
) -> Option<ChromePanelAction> {
let mut got_action: Option<ChromePanelAction> = None;
builder
.size(Size::exact(amt_open)) // collapsible sidebar
.size(Size::remainder()) // the main app contents
.clip(true)
.horizontal(|mut hstrip| {
hstrip.cell(|ui| {
let rect = ui.available_rect_before_wrap();
if !ui.visuals().dark_mode {
let rect = ui.available_rect_before_wrap();
ui.painter().rect(
rect,
0,
notedeck_ui::colors::ALMOST_WHITE,
egui::Stroke::new(0.0, Color32::TRANSPARENT),
egui::StrokeKind::Inside,
);
}
StripBuilder::new(ui)
.size(Size::remainder())
.size(Size::remainder())
.vertical(|mut vstrip| {
vstrip.cell(|ui| {
_ = ui.vertical_centered(|ui| {
self.topdown_sidebar(ui, app_ctx.i18n);
})
});
vstrip.cell(|ui| {
ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
if let Some(action) = bottomup_sidebar(self, app_ctx, ui) {
got_action = Some(action);
}
});
});
});
// vertical sidebar line
ui.painter().vline(
rect.right(),
rect.y_range(),
ui.visuals().widgets.noninteractive.bg_stroke,
);
});
hstrip.cell(|ui| {
/*
let rect = ui.available_rect_before_wrap();
ui.painter().rect(
rect,
0,
egui::Color32::RED,
egui::Stroke::new(1.0, egui::Color32::BLUE),
egui::StrokeKind::Inside,
);
*/
if let Some(action) = self.apps[self.active as usize].update(app_ctx, ui) {
chrome_handle_app_action(self, app_ctx, action, ui);
}
});
});
got_action
}
/// How far is the chrome panel expanded?
fn amount_open(&self, ui: &mut egui::Ui) -> f32 {
let open_id = egui::Id::new("chrome_open");
let side_panel_width: f32 = 74.0;
ui.ctx().animate_bool(open_id, self.open) * side_panel_width
}
fn toolbar_height() -> f32 {
48.0
}
/// On narrow layouts, we have a toolbar
fn toolbar_chrome(
&mut self,
ctx: &mut AppContext,
ui: &mut egui::Ui,
) -> Option<ChromePanelAction> {
let mut got_action: Option<ChromePanelAction> = None;
let amt_open = self.amount_open(ui);
StripBuilder::new(ui)
.size(Size::remainder()) // top cell
.size(Size::exact(Self::toolbar_height())) // bottom cell
.vertical(|mut strip| {
strip.strip(|builder| {
// the chrome panel is nested above the toolbar
got_action = self.panel(ctx, builder, amt_open);
});
strip.cell(|ui| {
if let Some(action) = self.toolbar(ui) {
got_action = Some(ChromePanelAction::Toolbar(action))
}
});
});
got_action
}
fn toolbar(&mut self, ui: &mut egui::Ui) -> Option<ToolbarAction> {
use egui_tabs::{TabColor, Tabs};
let rect = ui.available_rect_before_wrap();
ui.painter().hline(
rect.x_range(),
rect.top(),
ui.visuals().widgets.noninteractive.bg_stroke,
);
if !ui.visuals().dark_mode {
ui.painter().rect(
rect,
0,
notedeck_ui::colors::ALMOST_WHITE,
egui::Stroke::new(0.0, Color32::TRANSPARENT),
egui::StrokeKind::Inside,
);
}
let rs = Tabs::new(3)
.selected(self.tab_selected)
.hover_bg(TabColor::none())
.selected_fg(TabColor::none())
.selected_bg(TabColor::none())
.height(Self::toolbar_height())
.layout(Layout::centered_and_justified(egui::Direction::TopDown))
.show(ui, |ui, state| {
let index = state.index();
let mut action: Option<ToolbarAction> = None;
let btn_size: f32 = 20.0;
if index == 0 {
if home_button(ui, btn_size).clicked() {
action = Some(ToolbarAction::Home);
}
} else if index == 1 {
if let Some(dave) = self.get_dave() {
let rect = dave_toolbar_rect(ui, btn_size * 2.0);
if dave_button(dave.avatar_mut(), ui, rect).clicked() {
action = Some(ToolbarAction::Dave);
}
}
} else if index == 2 && notifications_button(ui, btn_size).clicked() {
action = Some(ToolbarAction::Notifications);
}
action
})
.inner();
for maybe_r in rs {
if maybe_r.inner.is_some() {
return maybe_r.inner;
}
}
None
}
/// Show the side menu or bar, depending on if we're on a narrow
/// or wide screen.
///
/// The side menu should hover over the screen, while the side bar
/// is collapsible but persistent on the screen.
fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> {
ui.spacing_mut().item_spacing.x = 0.0;
if notedeck::ui::is_narrow(ui.ctx()) {
self.toolbar_chrome(ctx, ui)
} else {
let amt_open = self.amount_open(ui);
self.panel(ctx, StripBuilder::new(ui), amt_open)
}
}
fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
// macos needs a bit of space to make room for window
// minimize/close buttons
if cfg!(target_os = "macos") {
ui.add_space(30.0);
} else {
// we still want *some* padding so that it aligns with the + button regardless
ui.add_space(notedeck_ui::constants::FRAME_MARGIN.into());
}
if ui.add(expand_side_panel_button()).clicked() {
//self.active = (self.active + 1) % (self.apps.len() as i32);
self.open = !self.open;
}
ui.add_space(4.0);
ui.add(milestone_name(i18n));
ui.add_space(16.0);
//let dark_mode = ui.ctx().style().visuals.dark_mode;
{
if columns_button(ui)
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
self.active = 0;
}
}
ui.add_space(32.0);
if let Some(dave) = self.get_dave() {
let rect = dave_sidebar_rect(ui);
let dave_resp = dave_button(dave.avatar_mut(), ui, rect)
.on_hover_cursor(egui::CursorIcon::PointingHand);
if dave_resp.clicked() {
self.switch_to_dave();
}
}
}
}
impl notedeck::App for Chrome {
fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> {
if let Some(action) = self.show(ctx, ui) {
action.process(ctx, self, ui);
}
// TODO: unify this constant with the columns side panel width. ui crate?
None
}
}
fn milestone_name<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|ui: &mut egui::Ui| -> egui::Response {
ui.vertical_centered(|ui| {
let font = egui::FontId::new(
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Tiny),
egui::FontFamily::Name(notedeck::fonts::NamedFontFamily::Bold.as_str().into()),
);
ui.add(
Label::new(
RichText::new(tr!(i18n, "BETA", "Beta version label"))
.color(ui.style().visuals.noninteractive().fg_stroke.color)
.font(font),
)
.selectable(false),
)
.on_hover_text(tr!(
i18n,
"Notedeck is a beta product. Expect bugs and contact us when you run into issues.",
"Beta product warning message"
))
.on_hover_cursor(egui::CursorIcon::Help)
})
.inner
}
}
fn expand_side_panel_button() -> impl Widget {
|ui: &mut egui::Ui| -> egui::Response {
let img_size = 40.0;
let img = app_images::damus_image()
.max_width(img_size)
.sense(egui::Sense::click());
ui.add(img)
}
}
fn expanding_button(
name: &'static str,
img_size: f32,
light_img: egui::Image,
dark_img: egui::Image,
ui: &mut egui::Ui,
) -> egui::Response {
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
let img = if ui.visuals().dark_mode {
dark_img
} else {
light_img
};
let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size));
let cur_img_size = helper.scale_1d_pos(img_size);
img.paint_at(
ui,
helper
.get_animation_rect()
.shrink((max_size - cur_img_size) / 2.0),
);
helper.take_animation_response()
}
fn support_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"help-button",
16.0,
app_images::help_light_image(),
app_images::help_dark_image(),
ui,
)
}
fn settings_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"settings-button",
32.0,
app_images::settings_light_image(),
app_images::settings_dark_image(),
ui,
)
}
fn notifications_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
expanding_button(
"notifications-button",
size,
app_images::notifications_light_image(),
app_images::notifications_dark_image(),
ui,
)
}
fn home_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
expanding_button(
"home-button",
size,
app_images::home_light_image(),
app_images::home_dark_image(),
ui,
)
}
fn columns_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"columns-button",
40.0,
app_images::columns_image(),
app_images::columns_image(),
ui,
)
}
fn dave_sidebar_rect(ui: &mut egui::Ui) -> Rect {
let size = vec2(60.0, 60.0);
let available = ui.available_rect_before_wrap();
let center_x = available.center().x;
let center_y = available.top();
egui::Rect::from_center_size(egui::pos2(center_x, center_y), size)
}
fn dave_toolbar_rect(ui: &mut egui::Ui, size: f32) -> Rect {
let size = vec2(size, size);
let available = ui.available_rect_before_wrap();
let center_x = available.center().x;
let center_y = available.center().y;
egui::Rect::from_center_size(egui::pos2(center_x, center_y), size)
}
fn dave_button(avatar: Option<&mut DaveAvatar>, ui: &mut egui::Ui, rect: Rect) -> egui::Response {
if let Some(avatar) = avatar {
avatar.render(rect, ui)
} else {
// plain icon if wgpu device not available??
ui.label("fixme")
}
}
pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str {
if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
url
} else {
notedeck::profile::no_pfp_url()
}
}
pub fn get_account_url<'a>(
txn: &'a nostrdb::Transaction,
ndb: &nostrdb::Ndb,
account: &UserAccount,
) -> &'a str {
if let Ok(profile) = ndb.get_profile_by_pubkey(txn, account.key.pubkey.bytes()) {
get_profile_url_owned(Some(profile))
} else {
get_profile_url_owned(None)
}
}
fn wallet_button() -> impl Widget {
|ui: &mut egui::Ui| -> egui::Response {
let img_size = 24.0;
let max_size = img_size * ICON_EXPANSION_MULTIPLE;
let img = if !ui.visuals().dark_mode {
app_images::wallet_light_image()
} else {
app_images::wallet_dark_image()
}
.max_width(img_size);
let helper = AnimationHelper::new(ui, "wallet-icon", vec2(max_size, max_size));
let cur_img_size = helper.scale_1d_pos(img_size);
img.paint_at(
ui,
helper
.get_animation_rect()
.shrink((max_size - cur_img_size) / 2.0),
);
helper.take_animation_response()
}
}
fn chrome_handle_app_action(
chrome: &mut Chrome,
ctx: &mut AppContext,
action: AppAction,
ui: &mut egui::Ui,
) {
match action {
AppAction::ToggleChrome => {
chrome.toggle();
}
AppAction::Note(note_action) => {
chrome.switch_to_columns();
let Some(columns) = chrome.get_columns_app() else {
return;
};
let txn = Transaction::new(ctx.ndb).unwrap();
let cols = columns
.decks_cache
.active_columns_mut(ctx.i18n, ctx.accounts)
.unwrap();
let m_action = notedeck_columns::actionbar::execute_and_process_note_action(
note_action,
ctx.ndb,
cols,
0,
&mut columns.timeline_cache,
&mut columns.threads,
ctx.note_cache,
ctx.pool,
&txn,
ctx.unknown_ids,
ctx.accounts,
ctx.global_wallet,
ctx.zaps,
ctx.img_cache,
ui,
);
if let Some(action) = m_action {
let col = cols.column_mut(0);
action.process(&mut col.router, &mut col.sheet_router);
}
}
}
}
fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
let helper = AnimationHelper::new(ui, "pfp-button", egui::vec2(max_size, max_size));
let min_pfp_size = ICON_WIDTH;
let cur_pfp_size = helper.scale_1d_pos(min_pfp_size);
let txn = Transaction::new(ctx.ndb).expect("should be able to create txn");
let profile_url = get_account_url(&txn, ctx.ndb, ctx.accounts.get_selected_account());
let mut widget = ProfilePic::new(ctx.img_cache, profile_url).size(cur_pfp_size);
ui.put(helper.get_animation_rect(), &mut widget);
helper.take_animation_response()
}
/// The section of the chrome sidebar that starts at the
/// bottom and goes up
fn bottomup_sidebar(
_chrome: &mut Chrome,
ctx: &mut AppContext,
ui: &mut egui::Ui,
) -> Option<ChromePanelAction> {
ui.add_space(8.0);
let pfp_resp = pfp_button(ctx, ui).on_hover_cursor(egui::CursorIcon::PointingHand);
let settings_resp = settings_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
let theme_action = match ui.ctx().theme() {
egui::Theme::Dark => {
let resp = ui
.add(Button::new("").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(tr!(
ctx.i18n,
"Switch to light mode",
"Hover text for light mode toggle button"
));
if resp.clicked() {
Some(ChromePanelAction::SaveTheme(ThemePreference::Light))
} else {
None
}
}
egui::Theme::Light => {
let resp = ui
.add(Button::new("🌙").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(tr!(
ctx.i18n,
"Switch to dark mode",
"Hover text for dark mode toggle button"
));
if resp.clicked() {
Some(ChromePanelAction::SaveTheme(ThemePreference::Dark))
} else {
None
}
}
};
let support_resp = support_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
let wallet_resp = ui
.add(wallet_button())
.on_hover_cursor(egui::CursorIcon::PointingHand);
if ctx.args.debug {
ui.weak(format!("{}", ctx.frame_history.fps() as i32));
ui.weak(format!(
"{:10.1}",
ctx.frame_history.mean_frame_time() * 1e3
));
#[cfg(feature = "memory")]
{
let mem_use = re_memory::MemoryUse::capture();
if let Some(counted) = mem_use.counted {
if ui
.label(format!("{}", format_bytes(counted as f64)))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
_chrome.show_memory_debug = !_chrome.show_memory_debug;
}
}
if let Some(resident) = mem_use.resident {
ui.weak(format!("{}", format_bytes(resident as f64)));
}
if _chrome.show_memory_debug {
egui::Window::new("Memory Debug").show(ui.ctx(), memory_debug_ui);
}
}
}
if pfp_resp.clicked() {
Some(ChromePanelAction::Account)
} else if settings_resp.clicked() {
Some(ChromePanelAction::Settings)
} else if theme_action.is_some() {
theme_action
} else if support_resp.clicked() {
Some(ChromePanelAction::Support)
} else if wallet_resp.clicked() {
Some(ChromePanelAction::Wallet)
} else {
None
}
}
#[cfg(feature = "memory")]
fn memory_debug_ui(ui: &mut egui::Ui) {
let Some(stats) = &re_memory::accounting_allocator::tracking_stats() else {
ui.label("re_memory::accounting_allocator::set_tracking_callstacks(true); not set!!");
return;
};
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label(format!(
"track_size_threshold {}",
stats.track_size_threshold
));
ui.label(format!(
"untracked {} {}",
stats.untracked.count,
format_bytes(stats.untracked.size as f64)
));
ui.label(format!(
"stochastically_tracked {} {}",
stats.stochastically_tracked.count,
format_bytes(stats.stochastically_tracked.size as f64),
));
ui.label(format!(
"fully_tracked {} {}",
stats.fully_tracked.count,
format_bytes(stats.fully_tracked.size as f64)
));
ui.label(format!(
"overhead {} {}",
stats.overhead.count,
format_bytes(stats.overhead.size as f64)
));
ui.separator();
for (i, callstack) in stats.top_callstacks.iter().enumerate() {
let full_bt = format!("{}", callstack.readable_backtrace);
let mut lines = full_bt.lines().skip(5);
let bt_header = lines.nth(0).map_or("??", |v| v);
let header = format!(
"#{} {bt_header} {}x {}",
i + 1,
callstack.extant.count,
format_bytes(callstack.extant.size as f64)
);
egui::CollapsingHeader::new(header)
.id_salt(("mem_cs", i))
.show(ui, |ui| {
ui.label(lines.collect::<Vec<_>>().join("\n"));
});
}
});
}
/// Pretty format a number of bytes by using SI notation (base2), e.g.
///
/// ```
/// # use re_format::format_bytes;
/// assert_eq!(format_bytes(123.0), "123 B");
/// assert_eq!(format_bytes(12_345.0), "12.1 KiB");
/// assert_eq!(format_bytes(1_234_567.0), "1.2 MiB");
/// assert_eq!(format_bytes(123_456_789.0), "118 MiB");
/// ```
#[cfg(feature = "memory")]
pub fn format_bytes(number_of_bytes: f64) -> String {
/// The minus character: <https://www.compart.com/en/unicode/U+2212>
/// Looks slightly different from the normal hyphen `-`.
const MINUS: char = '';
if number_of_bytes < 0.0 {
format!("{MINUS}{}", format_bytes(-number_of_bytes))
} else if number_of_bytes == 0.0 {
"0 B".to_owned()
} else if number_of_bytes < 1.0 {
format!("{number_of_bytes} B")
} else if number_of_bytes < 20.0 {
let is_integer = number_of_bytes.round() == number_of_bytes;
if is_integer {
format!("{number_of_bytes:.0} B")
} else {
format!("{number_of_bytes:.1} B")
}
} else if number_of_bytes < 10.0_f64.exp2() {
format!("{number_of_bytes:.0} B")
} else if number_of_bytes < 20.0_f64.exp2() {
let decimals = (10.0 * number_of_bytes < 20.0_f64.exp2()) as usize;
format!("{:.*} KiB", decimals, number_of_bytes / 10.0_f64.exp2())
} else if number_of_bytes < 30.0_f64.exp2() {
let decimals = (10.0 * number_of_bytes < 30.0_f64.exp2()) as usize;
format!("{:.*} MiB", decimals, number_of_bytes / 20.0_f64.exp2())
} else {
let decimals = (10.0 * number_of_bytes < 40.0_f64.exp2()) as usize;
format!("{:.*} GiB", decimals, number_of_bytes / 30.0_f64.exp2())
}
}