Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

re_ui crate to encapsulate our own egui theme #476

Merged
merged 12 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
204 changes: 133 additions & 71 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ opt-level = 2
debug = true

[patch.crates-io]
# 2022-11-30 - table improvements
eframe = { git = "https://github.com/emilk/egui", rev = "7133818c59269fee8cff8d182e776bda8858a727" }
egui = { git = "https://github.com/emilk/egui", rev = "7133818c59269fee8cff8d182e776bda8858a727" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "7133818c59269fee8cff8d182e776bda8858a727" }
egui-wgpu = { git = "https://github.com/emilk/egui", rev = "7133818c59269fee8cff8d182e776bda8858a727" }
emath = { git = "https://github.com/emilk/egui", rev = "7133818c59269fee8cff8d182e776bda8858a727" }
epaint = { git = "https://github.com/emilk/egui", rev = "7133818c59269fee8cff8d182e776bda8858a727" }
# 2022-12-06 - last fixes before egui 0.20 release
eframe = { git = "https://github.com/emilk/egui", rev = "9145893066d622aa6c39391346aef34c6ae103db" }
egui = { git = "https://github.com/emilk/egui", rev = "9145893066d622aa6c39391346aef34c6ae103db" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "9145893066d622aa6c39391346aef34c6ae103db" }
egui-wgpu = { git = "https://github.com/emilk/egui", rev = "9145893066d622aa6c39391346aef34c6ae103db" }
emath = { git = "https://github.com/emilk/egui", rev = "9145893066d622aa6c39391346aef34c6ae103db" }
epaint = { git = "https://github.com/emilk/egui", rev = "9145893066d622aa6c39391346aef34c6ae103db" }

# eframe = { path = "../../egui/crates/eframe" }
# egui = { path = "../../egui/crates/egui" }
Expand Down
3 changes: 1 addition & 2 deletions crates/re_renderer/examples/2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ impl framework::Example for Render2D {

fn new(re_ctx: &mut re_renderer::RenderContext) -> Self {
let rerun_logo =
image::load_from_memory(include_bytes!("../../re_viewer/data/logo_dark_mode.png"))
.unwrap();
image::load_from_memory(include_bytes!("../../re_ui/data/logo_dark_mode.png")).unwrap();

let mut image_data = rerun_logo.as_rgba8().unwrap().to_vec();

Expand Down
31 changes: 31 additions & 0 deletions crates/re_ui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "re_ui"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license = "(MIT OR Apache-2.0) AND OFL-1.1"
publish = false
include = [
"../../LICENSE-APACHE",
"../../LICENSE-MIT",
"**/*.rs",
"Cargo.toml",
"data/*",
]


[features]
default = []


[dependencies]
egui = { version = "0.19", features = ["extra_debug_asserts", "tracing"] }
egui_extras = { version = "0.19", features = ["tracing"] }
image = { version = "0.24", default-features = false, features = ["png"] }
parking_lot = "0.12"
serde = { version = "1", features = ["derive"] }
serde_json = "1"


[dev-dependencies]
eframe = { version = "0.19", default-features = false, features = ["wgpu"] }
1 change: 1 addition & 0 deletions crates/re_ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rerun GUI theme and helpers, built around [`egui`](https://www.egui.rs/).
File renamed without changes.
77 changes: 77 additions & 0 deletions crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
fn main() {
let native_options = eframe::NativeOptions {
initial_window_size: Some([1200.0, 800.0].into()),
follow_system_theme: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we're looked to dark theme in this sample? Why do we distinguish in a few places then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using dark theme everywhere for now. We had light theme support at the start, and I didn't see any reason to remove that support, since we will perhaps want to add it again in the future.

default_theme: eframe::Theme::Dark,

#[cfg(target_os = "macos")]
fullsize_content: re_ui::FULLSIZE_CONTENT,

..Default::default()
};

eframe::run_native(
"re_ui example app",
native_options,
Box::new(move |cc| {
let re_ui = re_ui::ReUi::load_and_apply(&cc.egui_ctx);
Box::new(ExampleApp { re_ui })
}),
);
}

pub struct ExampleApp {
re_ui: re_ui::ReUi,
}

impl eframe::App for ExampleApp {
fn update(&mut self, egui_ctx: &egui::Context, frame: &mut eframe::Frame) {
self.top_bar(egui_ctx, frame);

egui::CentralPanel::default().show(egui_ctx, |ui| {
egui::warn_if_debug_build(ui);
ui.label("Hello world!");
});
}
}

impl ExampleApp {
fn top_bar(&self, egui_ctx: &egui::Context, frame: &mut eframe::Frame) {
let panel_frame = {
egui::Frame {
inner_margin: egui::style::Margin::symmetric(8.0, 2.0),
fill: self.re_ui.design_tokens.top_bar_color,
..Default::default()
}
};

let native_pixels_per_point = frame.info().native_pixels_per_point;
let fullscreen = {
#[cfg(target_os = "macos")]
{
frame.info().window_info.fullscreen
}
#[cfg(not(target_os = "macos"))]
{
false
}
};
let top_bar_style = self
.re_ui
.top_bar_style(native_pixels_per_point, fullscreen);

egui::TopBottomPanel::top("top_bar")
.frame(panel_frame)
.exact_height(top_bar_style.height)
.show(egui_ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.set_height(top_bar_style.height);
ui.add_space(top_bar_style.indent);

ui.centered_and_justified(|ui| {
ui.strong("re_ui example app");
})
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,13 @@ pub struct DesignTokens {
}

impl DesignTokens {
#[allow(clippy::unused_self)]
pub fn panel_frame(&self, egui_ctx: &egui::Context) -> egui::Frame {
egui::Frame {
fill: egui_ctx.style().visuals.window_fill(),
inner_margin: egui::style::Margin::same(4.0),
..Default::default()
}
}

#[allow(clippy::unused_self)]
pub fn hovering_frame(&self, style: &egui::Style) -> egui::Frame {
egui::Frame {
inner_margin: egui::style::Margin::same(2.0),
outer_margin: egui::style::Margin::same(4.0),
rounding: 4.0.into(),
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default()
}
}

#[allow(clippy::unused_self)]
pub fn warning_text(&self, text: impl Into<String>, style: &egui::Style) -> egui::RichText {
egui::RichText::new(text)
.italics()
.color(style.visuals.warn_fg_color)
}

pub fn loop_selection_color() -> egui::Color32 {
egui::Color32::from_rgb(40, 200, 130)
/// Create [`DesignTokens`] and apply style to the given egui context.
pub fn load_and_apply(ctx: &egui::Context) -> Self {
apply_design_tokens(ctx)
}
}

pub(crate) fn apply_design_tokens(ctx: &egui::Context) -> DesignTokens {
fn apply_design_tokens(ctx: &egui::Context) -> DesignTokens {
let apply_font = true;
let apply_font_size = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ impl Icon {

pub const RIGHT_PANEL_TOGGLE: Icon = Icon::new(
"right_panel_toggle",
include_bytes!("../../data/icons/right_panel_toggle.png"),
include_bytes!("../data/icons/right_panel_toggle.png"),
);
pub const BOTTOM_PANEL_TOGGLE: Icon = Icon::new(
"bottom_panel_toggle",
include_bytes!("../../data/icons/bottom_panel_toggle.png"),
include_bytes!("../data/icons/bottom_panel_toggle.png"),
);
pub const LEFT_PANEL_TOGGLE: Icon = Icon::new(
"left_panel_toggle",
include_bytes!("../../data/icons/left_panel_toggle.png"),
include_bytes!("../data/icons/left_panel_toggle.png"),
);
184 changes: 184 additions & 0 deletions crates/re_ui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
mod design_tokens;
pub mod icons;
mod static_image_cache;

pub use design_tokens::DesignTokens;
pub use icons::Icon;
pub use static_image_cache::StaticImageCache;

// ---------------------------------------------------------------------------

/// If true, we fill the entire window, except for the close/maximize/minimize buttons in the top-left.
/// See <https://github.com/emilk/egui/pull/2049>
pub const FULLSIZE_CONTENT: bool = cfg!(target_os = "macos");

// ----------------------------------------------------------------------------

pub struct TopBarStyle {
/// Height of the top bar
pub height: f32,

/// Extra horizontal space in the top left corner to make room for
/// close/minimize/maximize buttons (on Mac)
pub indent: f32,
}

// ----------------------------------------------------------------------------

use parking_lot::Mutex;
use std::sync::Arc;

#[derive(Clone)]
pub struct ReUi {
pub egui_ctx: egui::Context,

/// Colors, styles etc loaded from a design_tokens.json
pub design_tokens: DesignTokens,

pub static_image_cache: Arc<Mutex<StaticImageCache>>,
}

impl ReUi {
/// Create [`ReUi`] and apply style to the given egui context.
pub fn load_and_apply(egui_ctx: &egui::Context) -> Self {
Self {
egui_ctx: egui_ctx.clone(),
design_tokens: DesignTokens::load_and_apply(egui_ctx),
static_image_cache: Arc::new(Mutex::new(StaticImageCache::default())),
}
}

pub fn rerun_logo(&self) -> Arc<egui_extras::RetainedImage> {
if self.egui_ctx.style().visuals.dark_mode {
self.static_image_cache.lock().get(
"logo_dark_mode",
include_bytes!("../data/logo_dark_mode.png"),
)
} else {
self.static_image_cache.lock().get(
"logo_light_mode",
include_bytes!("../data/logo_light_mode.png"),
)
}
}

#[allow(clippy::unused_self)]
pub fn panel_frame(&self) -> egui::Frame {
let style = self.egui_ctx.style();
egui::Frame {
fill: style.visuals.window_fill(),
inner_margin: egui::style::Margin::same(4.0),
..Default::default()
}
}

#[allow(clippy::unused_self)]
pub fn hovering_frame(&self) -> egui::Frame {
let style = self.egui_ctx.style();
egui::Frame {
inner_margin: egui::style::Margin::same(2.0),
outer_margin: egui::style::Margin::same(4.0),
rounding: 4.0.into(),
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default()
}
}

#[allow(clippy::unused_self)]
pub fn warning_text(&self, text: impl Into<String>) -> egui::RichText {
let style = self.egui_ctx.style();
egui::RichText::new(text)
.italics()
.color(style.visuals.warn_fg_color)
}

pub fn loop_selection_color() -> egui::Color32 {
egui::Color32::from_rgb(40, 200, 130)
}

/// Paint a watermark
pub fn paint_watermark(&self) {
use egui::*;
let logo = self.rerun_logo();
let screen_rect = self.egui_ctx.input().screen_rect;
let size = logo.size_vec2();
let rect = Align2::RIGHT_BOTTOM
.align_size_within_rect(size, screen_rect)
.translate(-Vec2::splat(16.0));
let mut mesh = Mesh::with_texture(logo.texture_id(&self.egui_ctx));
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
mesh.add_rect_with_uv(rect, uv, Color32::WHITE);
self.egui_ctx.debug_painter().add(Shape::mesh(mesh));
}

pub fn top_bar_style(
&self,
native_pixels_per_point: Option<f32>,
fullscreen: bool,
) -> TopBarStyle {
let gui_zoom = if let Some(native_pixels_per_point) = native_pixels_per_point {
native_pixels_per_point / self.egui_ctx.pixels_per_point()
} else {
1.0
};

// On Mac, we share the same space as the native red/yellow/green close/minimize/maximize buttons.
// This means we need to make room for them.
let make_room_for_window_buttons = {
#[cfg(target_os = "macos")]
{
crate::FULLSIZE_CONTENT && !fullscreen
}
#[cfg(not(target_os = "macos"))]
{
_ = fullscreen;
false
}
};

let native_buttons_size_in_native_scale = egui::vec2(64.0, 24.0); // source: I measured /emilk

let height = if make_room_for_window_buttons {
// Use more vertical space when zoomed in…
let height = native_buttons_size_in_native_scale.y;

// …but never shrink below the native button height when zoomed out.
height.max(gui_zoom * native_buttons_size_in_native_scale.y)
} else {
self.egui_ctx.style().spacing.interact_size.y
};

let indent = if make_room_for_window_buttons {
// Always use the same width measured in native GUI coordinates:
gui_zoom * native_buttons_size_in_native_scale.x
} else {
0.0
};

TopBarStyle { height, indent }
}

pub fn medium_icon_toggle_button(
&self,
ui: &mut egui::Ui,
icon: &Icon,
selected: &mut bool,
) -> egui::Response {
let size_points = egui::Vec2::splat(16.0); // TODO(emilk): get from design tokens

let image = self.static_image_cache.lock().get(icon.id, icon.png_bytes);
let texture_id = image.texture_id(ui.ctx());
let tint = if *selected {
ui.visuals().widgets.inactive.fg_stroke.color
} else {
egui::Color32::from_gray(100) // TODO(emilk): get from design tokens
};
let mut response = ui.add(egui::ImageButton::new(texture_id, size_points).tint(tint));
if response.clicked() {
*selected = !*selected;
response.mark_changed();
}
response
}
}