use std::collections::BTreeMap;
use ahash::HashMap;
use egui_tiles::{Behavior as _, EditAction};
use once_cell::sync::Lazy;
use re_entity_db::EntityPropertyMap;
use re_renderer::ScreenshotProcessor;
use re_space_view::SpaceViewBlueprint;
use re_ui::{Icon, ReUi};
use re_viewer_context::{
ContainerId, Item, SpaceViewClassIdentifier, SpaceViewClassRegistry, SpaceViewId,
SpaceViewState, SystemExecutionOutput, ViewQuery, ViewerContext,
};
use crate::container::blueprint_id_to_tile_id;
use crate::screenshot::handle_pending_space_view_screenshots;
use crate::{
add_space_view_or_container_modal::AddSpaceViewOrContainerModal, container::Contents,
icon_for_container_kind, space_view_entity_picker::SpaceViewEntityPicker,
system_execution::execute_systems_for_all_space_views, ViewportBlueprint,
};
pub struct PerSpaceViewState {
pub auto_properties: EntityPropertyMap,
pub space_view_state: Box<dyn SpaceViewState>,
}
#[derive(Default)]
pub struct ViewportState {
space_view_entity_modal: SpaceViewEntityPicker,
add_space_view_container_modal: AddSpaceViewOrContainerModal,
space_view_states: HashMap<SpaceViewId, PerSpaceViewState>,
candidate_drop_parent_container_id: Option<ContainerId>,
}
static DEFAULT_PROPS: Lazy<EntityPropertyMap> = Lazy::<EntityPropertyMap>::new(Default::default);
impl ViewportState {
pub fn space_view_state_mut(
&mut self,
space_view_class_registry: &SpaceViewClassRegistry,
space_view_id: SpaceViewId,
space_view_class: &SpaceViewClassIdentifier,
) -> &mut PerSpaceViewState {
self.space_view_states
.entry(space_view_id)
.or_insert_with(|| PerSpaceViewState {
auto_properties: Default::default(),
space_view_state: space_view_class_registry
.get_class_or_log_error(space_view_class)
.new_state(),
})
}
pub fn space_view_props(&self, space_view_id: SpaceViewId) -> &EntityPropertyMap {
self.space_view_states
.get(&space_view_id)
.map_or(&DEFAULT_PROPS, |state| &state.auto_properties)
}
pub fn is_candidate_drop_parent_container(&self, container_id: &ContainerId) -> bool {
self.candidate_drop_parent_container_id.as_ref() == Some(container_id)
}
}
#[derive(Clone, Debug)]
pub enum TreeAction {
AddSpaceView(SpaceViewId, Option<ContainerId>, Option<usize>),
AddContainer(egui_tiles::ContainerKind, Option<ContainerId>),
SetContainerKind(ContainerId, egui_tiles::ContainerKind),
FocusTab(SpaceViewId),
RemoveContents(Contents),
SimplifyContainer(ContainerId, egui_tiles::SimplificationOptions),
MoveContents {
contents_to_move: Contents,
target_container: ContainerId,
target_position_in_container: usize,
},
MoveContentsToNewContainer {
contents_to_move: Vec<Contents>,
new_container_kind: egui_tiles::ContainerKind,
target_container: ContainerId,
target_position_in_container: usize,
},
SetDropTarget(ContainerId),
}
fn tree_simplification_options() -> egui_tiles::SimplificationOptions {
egui_tiles::SimplificationOptions {
prune_empty_tabs: false,
all_panes_must_have_tabs: true,
prune_empty_containers: false,
prune_single_child_tabs: false,
prune_single_child_containers: false,
join_nested_linear_containers: true,
}
}
pub struct Viewport<'a, 'b> {
pub blueprint: &'a ViewportBlueprint,
pub state: &'b mut ViewportState,
pub tree: egui_tiles::Tree<SpaceViewId>,
pub tree_edited: bool,
tree_action_receiver: std::sync::mpsc::Receiver<TreeAction>,
tree_action_sender: std::sync::mpsc::Sender<TreeAction>,
}
impl<'a, 'b> Viewport<'a, 'b> {
pub fn new(
blueprint: &'a ViewportBlueprint,
state: &'b mut ViewportState,
space_view_class_registry: &SpaceViewClassRegistry,
tree_action_receiver: std::sync::mpsc::Receiver<TreeAction>,
tree_action_sender: std::sync::mpsc::Sender<TreeAction>,
) -> Self {
re_tracing::profile_function!();
let mut edited = false;
let tree = if blueprint.tree.is_empty() && !blueprint.space_views.is_empty() {
edited = true;
super::auto_layout::tree_from_space_views(
space_view_class_registry,
&blueprint.space_views,
)
} else {
blueprint.tree.clone()
};
Self {
blueprint,
state,
tree,
tree_edited: edited,
tree_action_receiver,
tree_action_sender,
}
}
pub fn show_add_remove_entities_modal(&mut self, space_view_id: SpaceViewId) {
self.state.space_view_entity_modal.open(space_view_id);
}
pub fn show_add_space_view_or_container_modal(&mut self, target_container: ContainerId) {
self.state
.add_space_view_container_modal
.open(target_container);
}
pub fn viewport_ui(&mut self, ui: &mut egui::Ui, ctx: &'a ViewerContext<'_>) {
self.state
.space_view_entity_modal
.ui(ui.ctx(), ctx, self.blueprint);
self.state
.add_space_view_container_modal
.ui(ui.ctx(), ctx, self.blueprint);
let Viewport {
blueprint, state, ..
} = self;
let is_zero_sized_viewport = ui.available_size().min_elem() <= 0.0;
if is_zero_sized_viewport {
return;
}
let mut maximized = blueprint.maximized;
if let Some(space_view_id) = blueprint.maximized {
if !blueprint.space_views.contains_key(&space_view_id) {
maximized = None;
} else if let Some(tile_id) = blueprint.tree.tiles.find_pane(&space_view_id) {
if !blueprint.tree.tiles.is_visible(tile_id) {
maximized = None;
}
}
}
let mut maximized_tree;
let tree = if let Some(space_view_id) = blueprint.maximized {
let mut tiles = egui_tiles::Tiles::default();
let root = tiles.insert_pane(space_view_id);
maximized_tree = egui_tiles::Tree::new("viewport_tree", root, tiles);
&mut maximized_tree
} else {
&mut self.tree
};
let executed_systems_per_space_view =
execute_systems_for_all_space_views(ctx, tree, &blueprint.space_views);
let contents_per_tile_id = blueprint
.contents_iter()
.map(|contents| (contents.as_tile_id(), contents))
.collect();
ui.scope(|ui| {
ui.spacing_mut().item_spacing.x = re_ui::ReUi::view_padding();
re_tracing::profile_scope!("tree.ui");
let mut tab_viewer = TabViewer {
viewport_state: state,
ctx,
space_views: &blueprint.space_views,
maximized: &mut maximized,
edited: false,
executed_systems_per_space_view,
contents_per_tile_id,
tree_action_sender: self.tree_action_sender.clone(),
root_container_id: self.blueprint.root_container,
};
tree.ui(&mut tab_viewer, ui);
let is_dragging_a_tile = tree.dragged_id(ui.ctx()).is_some();
if tab_viewer.edited || is_dragging_a_tile {
if blueprint.auto_layout() {
re_log::trace!(
"The user is manipulating the egui_tiles tree - will no longer auto-layout"
);
}
blueprint.set_auto_layout(false, ctx);
}
self.tree_edited |= tab_viewer.edited;
});
self.blueprint.set_maximized(maximized, ctx);
}
pub fn on_frame_start(&mut self, ctx: &ViewerContext<'_>) {
re_tracing::profile_function!();
for space_view in self.blueprint.space_views.values() {
let PerSpaceViewState {
auto_properties,
space_view_state,
} = self.state.space_view_state_mut(
ctx.space_view_class_registry,
space_view.id,
space_view.class_identifier(),
);
#[allow(clippy::blocks_in_if_conditions)]
while ScreenshotProcessor::next_readback_result(
ctx.render_ctx,
space_view.id.gpu_readback_id(),
|data, extent, mode| {
handle_pending_space_view_screenshots(space_view, data, extent, mode);
},
)
.is_some()
{}
space_view.on_frame_start(ctx, space_view_state.as_mut(), auto_properties);
}
if self.blueprint.auto_space_views() {
self.spawn_heuristic_space_views(ctx);
}
}
fn spawn_heuristic_space_views(&mut self, ctx: &ViewerContext<'_>) {
re_tracing::profile_function!();
for entry in ctx.space_view_class_registry.iter_registry() {
let class_id = entry.class.identifier();
let spawn_heuristics = entry.class.spawn_heuristics(ctx);
re_tracing::profile_scope!("filter_recommendations_for", class_id);
let existing_path_filters = self
.blueprint
.space_views
.values()
.filter(|space_view| space_view.class_identifier() == &class_id)
.map(|space_view| space_view.entity_path_filter())
.collect::<Vec<_>>();
let recommended_space_views = spawn_heuristics
.recommended_space_views
.into_iter()
.filter(|recommended_view| {
existing_path_filters.iter().all(|existing_filter| {
!existing_filter.is_superset_of(&recommended_view.query_filter)
})
})
.collect::<Vec<_>>();
let final_recommendations =
recommended_space_views
.iter()
.enumerate()
.filter(|(j, candidate)| {
recommended_space_views
.iter()
.enumerate()
.all(|(i, other)| {
i == *j
|| !other.query_filter.is_superset_of(&candidate.query_filter)
})
});
self.blueprint.add_space_views(
final_recommendations.map(|(_, recommendation)| {
SpaceViewBlueprint::new(
class_id,
&recommendation.root,
re_space_view::DataQueryBlueprint::new(
class_id,
recommendation.query_filter.clone(),
),
)
}),
ctx,
None,
None,
);
}
}
pub fn update_and_sync_tile_tree_to_blueprint(mut self, ctx: &ViewerContext<'_>) {
let mut reset = false;
self.state.candidate_drop_parent_container_id = None;
for tree_action in self.tree_action_receiver.try_iter() {
re_log::trace!("Processing tree action: {tree_action:?}");
match tree_action {
TreeAction::AddSpaceView(space_view_id, parent_container, position_in_parent) => {
if self.blueprint.auto_layout() {
re_log::trace!(
"Added a space view with no user edits yet - will re-run auto-layout"
);
reset = true;
} else if let Some(parent_id) =
parent_container.or(self.blueprint.root_container)
{
let tile_id = self.tree.tiles.insert_pane(space_view_id);
let container_tile_id = blueprint_id_to_tile_id(&parent_id);
if let Some(egui_tiles::Tile::Container(container)) =
self.tree.tiles.get_mut(container_tile_id)
{
re_log::trace!("Inserting new space view into root container");
container.add_child(tile_id);
if let Some(position_in_parent) = position_in_parent {
self.tree.move_tile_to_container(
tile_id,
container_tile_id,
position_in_parent,
true,
);
}
} else {
re_log::trace!("Root was not a container - will re-run auto-layout");
reset = true;
}
} else {
re_log::trace!("No root found - will re-run auto-layout");
}
self.tree_edited = true;
}
TreeAction::AddContainer(container_kind, parent_container) => {
if let Some(parent_id) = parent_container.or(self.blueprint.root_container) {
let tile_id = self
.tree
.tiles
.insert_container(egui_tiles::Container::new(container_kind, vec![]));
if let Some(egui_tiles::Tile::Container(container)) =
self.tree.tiles.get_mut(blueprint_id_to_tile_id(&parent_id))
{
re_log::trace!("Inserting new space view into container {parent_id:?}");
container.add_child(tile_id);
} else {
re_log::trace!(
"Parent or root was not a container - will re-run auto-layout"
);
reset = true;
}
} else {
re_log::trace!("No root found - will re-run auto-layout");
}
self.tree_edited = true;
}
TreeAction::SetContainerKind(container_id, container_kind) => {
if let Some(egui_tiles::Tile::Container(container)) = self
.tree
.tiles
.get_mut(blueprint_id_to_tile_id(&container_id))
{
re_log::trace!("Mutating container {container_id:?} to {container_kind:?}");
container.set_kind(container_kind);
} else {
re_log::trace!("No root found - will re-run auto-layout");
}
self.tree_edited = true;
}
TreeAction::FocusTab(space_view_id) => {
let found = self.tree.make_active(|_, tile| match tile {
egui_tiles::Tile::Pane(this_space_view_id) => {
*this_space_view_id == space_view_id
}
egui_tiles::Tile::Container(_) => false,
});
re_log::trace!(
"Found tab to focus on for space view ID {space_view_id}: {found}"
);
self.tree_edited = true;
}
TreeAction::RemoveContents(contents) => {
let tile_id = contents.as_tile_id();
for tile in self.tree.remove_recursively(tile_id) {
re_log::trace!("Removing tile {tile_id:?}");
if let egui_tiles::Tile::Pane(space_view_id) = tile {
re_log::trace!("Removing space view {space_view_id}");
self.tree.tiles.remove(tile_id);
self.blueprint.remove_space_view(&space_view_id, ctx);
}
}
if Some(tile_id) == self.tree.root {
self.tree.root = None;
}
self.tree_edited = true;
}
TreeAction::SimplifyContainer(container_id, options) => {
re_log::trace!("Simplifying tree with options: {options:?}");
let tile_id = blueprint_id_to_tile_id(&container_id);
self.tree.simplify_children_of_tile(tile_id, &options);
self.tree_edited = true;
}
TreeAction::MoveContents {
contents_to_move,
target_container,
target_position_in_container,
} => {
re_log::trace!(
"Moving {contents_to_move:?} to container {target_container:?} at pos \
{target_position_in_container}"
);
let contents_tile_id = contents_to_move.as_tile_id();
let target_container_tile_id = blueprint_id_to_tile_id(&target_container);
self.tree.move_tile_to_container(
contents_tile_id,
target_container_tile_id,
target_position_in_container,
true,
);
self.tree_edited = true;
}
TreeAction::MoveContentsToNewContainer {
contents_to_move,
new_container_kind,
target_container,
target_position_in_container,
} => {
let new_container_tile_id = self
.tree
.tiles
.insert_container(egui_tiles::Container::new(new_container_kind, vec![]));
let target_container_tile_id = blueprint_id_to_tile_id(&target_container);
self.tree.move_tile_to_container(
new_container_tile_id,
target_container_tile_id,
target_position_in_container,
true, );
for (pos, content) in contents_to_move.into_iter().enumerate() {
self.tree.move_tile_to_container(
content.as_tile_id(),
new_container_tile_id,
pos,
true, );
}
self.tree_edited = true;
}
TreeAction::SetDropTarget(container_id) => {
self.state.candidate_drop_parent_container_id = Some(container_id);
}
}
}
if reset {
re_log::trace!("Clearing the blueprint tree to force reset on the next frame");
self.tree = egui_tiles::Tree::empty("viewport_tree");
self.tree_edited = true;
}
if self.tree_edited {
let options = tree_simplification_options();
self.tree.simplify(&options);
self.blueprint.save_tree_as_containers(&self.tree, ctx);
}
}
#[inline]
pub fn is_item_valid(&self, item: &Item) -> bool {
self.blueprint.is_item_valid(item)
}
}
struct TabViewer<'a, 'b> {
viewport_state: &'a mut ViewportState,
ctx: &'a ViewerContext<'b>,
space_views: &'a BTreeMap<SpaceViewId, SpaceViewBlueprint>,
maximized: &'a mut Option<SpaceViewId>,
root_container_id: Option<ContainerId>,
tree_action_sender: std::sync::mpsc::Sender<TreeAction>,
executed_systems_per_space_view: HashMap<SpaceViewId, (ViewQuery<'a>, SystemExecutionOutput)>,
contents_per_tile_id: HashMap<egui_tiles::TileId, Contents>,
edited: bool,
}
impl<'a, 'b> egui_tiles::Behavior<SpaceViewId> for TabViewer<'a, 'b> {
fn pane_ui(
&mut self,
ui: &mut egui::Ui,
_tile_id: egui_tiles::TileId,
space_view_id: &mut SpaceViewId,
) -> egui_tiles::UiResponse {
re_tracing::profile_function!();
let Some(space_view_blueprint) = self.space_views.get(space_view_id) else {
return Default::default();
};
let is_zero_sized_viewport = ui.available_size().min_elem() <= 0.0;
if is_zero_sized_viewport {
return Default::default();
}
let Some(latest_at) = self.ctx.rec_cfg.time_ctrl.read().time_int() else {
ui.centered_and_justified(|ui| {
ui.weak("No time selected");
});
return Default::default();
};
let (query, system_output) =
self.executed_systems_per_space_view.remove(space_view_id).unwrap_or_else(|| {
if cfg!(debug_assertions) {
re_log::warn_once!(
"Visualizers for space view {:?} haven't been executed prior to display. This should never happen, please report a bug.",
space_view_blueprint.display_name_or_default()
);
}
let highlights =
crate::space_view_highlights::highlights_for_space_view(self.ctx, *space_view_id);
crate::system_execution::execute_systems_for_space_view(
self.ctx,
space_view_blueprint,
latest_at,
highlights,
)
});
let PerSpaceViewState {
auto_properties: _,
space_view_state,
} = self.viewport_state.space_view_state_mut(
self.ctx.space_view_class_registry,
space_view_blueprint.id,
space_view_blueprint.class_identifier(),
);
space_view_blueprint.scene_ui(
space_view_state.as_mut(),
self.ctx,
ui,
&query,
system_output,
);
Default::default()
}
fn tab_title_for_pane(&mut self, space_view_id: &SpaceViewId) -> egui::WidgetText {
if let Some(space_view) = self.space_views.get(space_view_id) {
space_view.display_name_or_default().as_ref().into()
} else {
re_log::warn_once!("SpaceViewId missing during egui_tiles");
self.ctx.re_ui.error_text("Internal error").into()
}
}
#[allow(clippy::fn_params_excessive_bools)]
fn tab_ui(
&mut self,
tiles: &egui_tiles::Tiles<SpaceViewId>,
ui: &mut egui::Ui,
id: egui::Id,
tile_id: egui_tiles::TileId,
active: bool,
is_being_dragged: bool,
) -> egui::Response {
let tab_widget = TabWidget::new(self, ui, tiles, tile_id, active, 1.0);
let response = ui.interact(tab_widget.rect, id, egui::Sense::click_and_drag());
if ui.is_rect_visible(tab_widget.rect) && !is_being_dragged {
tab_widget.paint(ui);
}
match tiles.get(tile_id) {
Some(egui_tiles::Tile::Pane(space_view_id)) => {
self.ctx
.select_hovered_on_click(&response, Item::SpaceView(*space_view_id));
}
Some(egui_tiles::Tile::Container(_)) => {
if let Some(Contents::Container(container_id)) =
self.contents_per_tile_id.get(&tile_id)
{
self.ctx
.select_hovered_on_click(&response, Item::Container(*container_id));
}
}
None => {}
}
response
}
fn drag_ui(
&mut self,
tiles: &egui_tiles::Tiles<SpaceViewId>,
ui: &mut egui::Ui,
tile_id: egui_tiles::TileId,
) {
let tab_widget = TabWidget::new(self, ui, tiles, tile_id, true, 0.5);
let frame = egui::Frame {
inner_margin: egui::Margin::same(0.),
outer_margin: egui::Margin::same(0.),
rounding: egui::Rounding::ZERO,
shadow: Default::default(),
fill: egui::Color32::TRANSPARENT,
stroke: egui::Stroke::NONE,
};
frame.show(ui, |ui| {
tab_widget.paint(ui);
});
}
fn retain_pane(&mut self, space_view_id: &SpaceViewId) -> bool {
self.space_views.contains_key(space_view_id)
}
fn top_bar_right_ui(
&mut self,
tiles: &egui_tiles::Tiles<SpaceViewId>,
ui: &mut egui::Ui,
_tile_id: egui_tiles::TileId,
tabs: &egui_tiles::Tabs,
_scroll_offset: &mut f32,
) {
let Some(active) = tabs.active.and_then(|active| tiles.get(active)) else {
return;
};
let egui_tiles::Tile::Pane(space_view_id) = active else {
return;
};
let space_view_id = *space_view_id;
let Some(space_view) = self.space_views.get(&space_view_id) else {
return;
};
let num_space_views = tiles.tiles().filter(|tile| tile.is_pane()).count();
ui.add_space(8.0);
if *self.maximized == Some(space_view_id) {
if self
.ctx
.re_ui
.small_icon_button(ui, &re_ui::icons::MINIMIZE)
.on_hover_text("Restore - show all spaces")
.clicked()
{
*self.maximized = None;
}
} else if num_space_views > 1 {
if self
.ctx
.re_ui
.small_icon_button(ui, &re_ui::icons::MAXIMIZE)
.on_hover_text("Maximize Space View")
.clicked()
{
*self.maximized = Some(space_view_id);
}
}
let help_text = space_view
.class(self.ctx.space_view_class_registry)
.help_text(self.ctx.re_ui);
re_ui::help_hover_button(ui).on_hover_text(help_text);
}
fn tab_bar_color(&self, _visuals: &egui::Visuals) -> egui::Color32 {
self.ctx.re_ui.design_tokens.tab_bar_color
}
fn dragged_overlay_color(&self, visuals: &egui::Visuals) -> egui::Color32 {
visuals.panel_fill.gamma_multiply(0.5)
}
fn drag_preview_stroke(&self, _visuals: &egui::Visuals) -> egui::Stroke {
egui::Stroke::new(1.0, egui::Color32::WHITE.gamma_multiply(0.5))
}
fn drag_preview_color(&self, _visuals: &egui::Visuals) -> egui::Color32 {
egui::Color32::WHITE.gamma_multiply(0.1)
}
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
re_ui::ReUi::title_bar_height()
}
fn simplification_options(&self) -> egui_tiles::SimplificationOptions {
tree_simplification_options()
}
fn on_edit(&mut self, edit_action: egui_tiles::EditAction) {
match edit_action {
EditAction::TileDropped => {
if let Some(root_container_id) = self.root_container_id {
if self
.tree_action_sender
.send(TreeAction::SimplifyContainer(
root_container_id,
egui_tiles::SimplificationOptions {
prune_empty_tabs: true,
prune_empty_containers: false,
prune_single_child_tabs: true,
prune_single_child_containers: false,
all_panes_must_have_tabs: true,
join_nested_linear_containers: false,
},
))
.is_err()
{
re_log::warn_once!(
"Channel between ViewportBlueprint and Viewport is broken"
);
}
}
self.edited = true;
}
EditAction::TabSelected | EditAction::TileResized => {
self.edited = true;
}
EditAction::TileDragged => {
}
}
}
}
struct TabWidget {
galley: std::sync::Arc<egui::Galley>,
rect: egui::Rect,
galley_rect: egui::Rect,
icon: &'static Icon,
icon_size: egui::Vec2,
icon_rect: egui::Rect,
bg_color: egui::Color32,
text_color: egui::Color32,
unnamed_style: bool,
}
impl TabWidget {
fn new<'a>(
tab_viewer: &'a mut TabViewer<'_, '_>,
ui: &'a mut egui::Ui,
tiles: &'a egui_tiles::Tiles<SpaceViewId>,
tile_id: egui_tiles::TileId,
active: bool,
gamma: f32,
) -> Self {
struct TabDesc {
label: egui::WidgetText,
user_named: bool,
icon: &'static re_ui::Icon,
item: Option<Item>,
}
let tab_desc = match tiles.get(tile_id) {
Some(egui_tiles::Tile::Pane(space_view_id)) => {
if let Some(space_view) = tab_viewer.space_views.get(space_view_id) {
TabDesc {
label: tab_viewer.tab_title_for_pane(space_view_id),
user_named: space_view.display_name.is_some(),
icon: space_view
.class(tab_viewer.ctx.space_view_class_registry)
.icon(),
item: Some(Item::SpaceView(*space_view_id)),
}
} else {
re_log::warn_once!("Space View {space_view_id} not found");
TabDesc {
label: tab_viewer.ctx.re_ui.error_text("Unknown Space View").into(),
icon: &re_ui::icons::SPACE_VIEW_GENERIC,
user_named: false,
item: None,
}
}
}
Some(egui_tiles::Tile::Container(container)) => {
if let Some(Contents::Container(container_id)) =
tab_viewer.contents_per_tile_id.get(&tile_id)
{
TabDesc {
label: format!("{:?}", container.kind()).into(),
user_named: false,
icon: icon_for_container_kind(&container.kind()),
item: Some(Item::Container(*container_id)),
}
} else {
if container.kind() == egui_tiles::ContainerKind::Tabs {
if let Some(tile_id) = container.only_child() {
return Self::new(tab_viewer, ui, tiles, tile_id, active, gamma);
}
}
re_log::warn_once!("Container for tile ID {tile_id:?} not found");
TabDesc {
label: tab_viewer.ctx.re_ui.error_text("Unknown Container").into(),
icon: &re_ui::icons::SPACE_VIEW_GENERIC,
user_named: false,
item: None,
}
}
}
None => {
re_log::warn_once!("Tile {tile_id:?} not found");
TabDesc {
label: tab_viewer.ctx.re_ui.error_text("Internal error").into(),
icon: &re_ui::icons::SPACE_VIEW_UNKNOWN,
user_named: false,
item: None,
}
}
};
let hovered = tab_desc
.item
.as_ref()
.map_or(false, |item| tab_viewer.ctx.hovered().contains_item(item));
let selected = tab_desc
.item
.as_ref()
.map_or(false, |item| tab_viewer.ctx.selection().contains_item(item));
let icon_size = ReUi::small_icon_size();
let icon_width_plus_padding = icon_size.x + ReUi::text_to_icon_padding();
let text = if !tab_desc.user_named {
tab_desc.label.italics()
} else {
tab_desc.label
};
let font_id = egui::TextStyle::Button.resolve(ui.style());
let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
let x_margin = tab_viewer.tab_title_spacing(ui.visuals());
let (_, rect) = ui.allocate_space(egui::vec2(
galley.size().x + 2.0 * x_margin + icon_width_plus_padding,
ReUi::title_bar_height(),
));
let galley_rect = egui::Rect::from_two_pos(
rect.min + egui::vec2(icon_width_plus_padding, 0.0),
rect.max,
);
let icon_rect = egui::Rect::from_center_size(
egui::pos2(rect.left() + x_margin + icon_size.x / 2.0, rect.center().y),
icon_size,
);
let bg_color = if selected {
ui.visuals().selection.bg_fill
} else if hovered {
ui.visuals().widgets.hovered.bg_fill
} else {
tab_viewer.tab_bar_color(ui.visuals())
};
let bg_color = bg_color.gamma_multiply(gamma);
let text_color = tab_viewer
.tab_text_color(ui.visuals(), tiles, tile_id, active)
.gamma_multiply(gamma);
Self {
galley,
rect,
galley_rect,
icon: tab_desc.icon,
icon_size,
icon_rect,
bg_color,
text_color,
unnamed_style: !tab_desc.user_named,
}
}
fn paint(self, ui: &egui::Ui) {
ui.painter()
.rect(self.rect, 0.0, self.bg_color, egui::Stroke::NONE);
let icon_image = self
.icon
.as_image()
.fit_to_exact_size(self.icon_size)
.tint(self.text_color);
icon_image.paint_at(ui, self.icon_rect);
let label_color = if self.unnamed_style {
self.text_color.gamma_multiply(0.5)
} else {
self.text_color
};
ui.painter().galley(
egui::Align2::CENTER_CENTER
.align_size_within_rect(self.galley.size(), self.galley_rect)
.min,
self.galley,
label_color,
);
}
}