use ahash::HashMap;
use egui::remap_clamp;
use egui_tiles::{Behavior as _, EditAction};
use itertools::Either;
use re_context_menu::{SelectionUpdateBehavior, context_menu_ui_for_item};
use re_log_types::{EntityPath, ResolvedEntityPathRule, RuleEffect};
use re_ui::{
ContextExt as _, Help, Icon, IconText, UICommandSender as _, UiExt as _,
design_tokens_of_visuals, icons,
};
use re_view::controls::TOGGLE_MAXIMIZE_VIEW;
use re_viewer_context::{
Contents, DragAndDropFeedback, DragAndDropPayload, Item, PublishedViewInfo, SystemCommand,
SystemCommandSender as _, SystemExecutionOutput, ViewId, ViewQuery, ViewStates, ViewerContext,
icon_for_container_kind,
};
use re_viewport_blueprint::{
ViewBlueprint, ViewportBlueprint, ViewportCommand, create_entity_add_info,
};
use crate::system_execution::{execute_systems_for_all_views, execute_systems_for_view};
pub struct ViewportUi {
pub blueprint: ViewportBlueprint,
}
impl ViewportUi {
pub fn new(blueprint: ViewportBlueprint) -> Self {
Self { blueprint }
}
pub fn viewport_ui(
&self,
ui: &mut egui::Ui,
ctx: &ViewerContext<'_>,
view_states: &mut ViewStates,
) {
let tokens = ui.tokens();
let Self { blueprint } = self;
let is_zero_sized_viewport = ui.available_size().min_elem() <= 0.0;
if is_zero_sized_viewport || !ui.is_visible() {
return;
}
let mut maximized = blueprint.maximized;
if let Some(view_id) = blueprint.maximized {
if !blueprint.views.contains_key(&view_id) {
maximized = None;
} else if let Some(tile_id) = blueprint.tree.tiles.find_pane(&view_id)
&& !blueprint.tree.tiles.is_visible(tile_id)
{
maximized = None;
}
}
let (animating_view_id, animated_rect) = ui
.data(|data| data.get_temp::<MaximizeAnimationState>(egui::Id::NULL))
.unwrap_or_default()
.animated_view_and_rect(ui.ctx(), ui.max_rect());
let mut tree = if let Some(view_id) = blueprint.maximized.or(animating_view_id) {
let mut tiles = egui_tiles::Tiles::default();
let tile_id = Contents::View(view_id).as_tile_id();
tiles.insert(tile_id, egui_tiles::Tile::Pane(view_id));
egui_tiles::Tree::new("viewport_tree", tile_id, tiles)
} else {
blueprint.tree.clone()
};
view_states.reset_visualizer_errors();
let executed_systems_per_view =
execute_systems_for_all_views(ctx, &tree, &blueprint.views, view_states);
#[expect(clippy::iter_over_hash_type)] for (view_id, (_, system_output)) in &executed_systems_per_view {
view_states.report_visualizer_errors(*view_id, system_output);
}
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 = tokens.view_padding() as f32;
re_tracing::profile_scope!("tree.ui");
*ui = ui.new_child(egui::UiBuilder::new().max_rect(animated_rect));
let mut egui_tiles_delegate = TilesDelegate {
view_states,
ctx,
viewport_blueprint: blueprint,
maximized: &mut maximized,
executed_systems_per_view,
contents_per_tile_id,
edited: false,
tile_dropped: false,
};
tree.ui(&mut egui_tiles_delegate, ui);
let dragged_payload = egui::DragAndDrop::payload::<DragAndDropPayload>(ui.ctx());
let dragged_payload = dragged_payload.as_ref().and_then(|payload| {
if let DragAndDropPayload::Entities { entities } = payload.as_ref() {
Some(entities)
} else {
None
}
});
let mut hover_rects = Vec::new();
let mut selection_rects = Vec::new();
let mut drop_target_rects = Vec::new();
for contents in blueprint.contents_iter() {
let tile_id = contents.as_tile_id();
if let Some(rect) = tree.tiles.rect(tile_id) {
let item = contents.as_item();
let mut hovered = ctx.hovered().contains_item(&item);
let selected = ctx.selection().contains_item(&item);
let pointer_in_rect = ui.rect_contains_pointer(rect);
if pointer_in_rect {
hovered = false;
}
let should_display_drop_destination_frame = if pointer_in_rect
&& let Some(view_id) = contents.as_view_id()
&& let Some(view_blueprint) = self.blueprint.view(&view_id)
&& let Some(dragged_payload) = dragged_payload
{
Self::handle_drop_entities_to_view(ctx, view_blueprint, dragged_payload)
} else {
false
};
if matches!(contents, Contents::View(_))
&& !should_display_drop_destination_frame
{
continue;
}
if should_display_drop_destination_frame {
drop_target_rects.push(rect);
} else if hovered {
hover_rects.push(rect);
} else if selected {
selection_rects.push(rect);
}
}
}
let top_layer_id = egui::LayerId::new(ui.layer_id().order, ui.id().with("child_id"));
ui.ctx().set_sublayer(ui.layer_id(), top_layer_id); let painter = ui.painter().clone().with_layer_id(top_layer_id);
let selection_stroke = ui.ctx().selection_stroke();
for rect in selection_rects {
painter.rect_stroke(rect, 0.0, selection_stroke, egui::StrokeKind::Inside);
}
let hover_stroke = ui.ctx().hover_stroke();
for rect in hover_rects {
painter.rect_stroke(rect, 0.0, hover_stroke, egui::StrokeKind::Inside);
}
let drop_target_stroke = tokens.drop_target_container_stroke;
for rect in drop_target_rects {
painter.rect_stroke(rect, 0.0, drop_target_stroke, egui::StrokeKind::Inside);
painter.rect_filled(
rect.shrink(drop_target_stroke.width),
0.0,
drop_target_stroke.color.gamma_multiply(0.1),
);
}
if blueprint.maximized.is_none() {
let is_dragging_a_tile = tree.dragged_id(ui.ctx()).is_some();
if egui_tiles_delegate.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);
}
if egui_tiles_delegate.edited {
if egui_tiles_delegate.tile_dropped {
tree.simplify(&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,
});
}
self.blueprint
.deferred_commands
.lock()
.push(ViewportCommand::SetTree(tree));
}
}
});
self.blueprint.set_maximized(maximized, ctx);
}
fn handle_drop_entities_to_view(
ctx: &ViewerContext<'_>,
view_blueprint: &ViewBlueprint,
entities: &[EntityPath],
) -> bool {
let add_info = create_entity_add_info(
ctx,
ctx.recording().tree(),
view_blueprint,
ctx.lookup_query_result(view_blueprint.id),
);
let can_entity_be_added = |entity: &EntityPath| {
add_info
.get(entity)
.is_some_and(|info| info.can_add_self_or_descendant.is_compatible_and_missing())
};
let any_is_visualizable = entities.iter().any(can_entity_be_added);
ctx.drag_and_drop_manager
.set_feedback(if any_is_visualizable {
DragAndDropFeedback::Accept
} else {
DragAndDropFeedback::Reject
});
if !any_is_visualizable {
return false;
}
if ctx.egui_ctx().input(|i| i.pointer.any_released()) {
egui::DragAndDrop::clear_payload(ctx.egui_ctx());
view_blueprint
.contents
.mutate_entity_path_filter(ctx, |filter| {
for entity in entities {
if can_entity_be_added(entity) {
filter.add_rule(
RuleEffect::Include,
ResolvedEntityPathRule::including_subtree(entity),
);
}
}
});
ctx.command_sender()
.send_system(SystemCommand::set_selection(Item::View(view_blueprint.id)));
false
} else {
any_is_visualizable
}
}
pub fn on_frame_start(&self, ctx: &ViewerContext<'_>) {
re_tracing::profile_function!();
self.blueprint.spawn_heuristic_views(ctx);
}
pub fn save_to_blueprint_store(self, ctx: &ViewerContext<'_>) {
self.blueprint.save_to_blueprint_store(ctx);
}
}
struct TilesDelegate<'a, 'b> {
view_states: &'a mut ViewStates,
ctx: &'a ViewerContext<'b>,
viewport_blueprint: &'a ViewportBlueprint,
maximized: &'a mut Option<ViewId>,
executed_systems_per_view: HashMap<ViewId, (ViewQuery<'a>, SystemExecutionOutput)>,
contents_per_tile_id: HashMap<egui_tiles::TileId, Contents>,
edited: bool,
tile_dropped: bool,
}
impl<'a> egui_tiles::Behavior<ViewId> for TilesDelegate<'a, '_> {
fn pane_ui(
&mut self,
ui: &mut egui::Ui,
_tile_id: egui_tiles::TileId,
view_id: &mut ViewId,
) -> egui_tiles::UiResponse {
re_tracing::profile_function!();
let Some(view_blueprint) = self.viewport_blueprint.view(view_id) else {
return Default::default();
};
let is_zero_sized_viewport = ui.available_size().min_elem() <= 0.0;
if is_zero_sized_viewport || !ui.is_visible() {
return Default::default();
}
let (query, system_output) = self.executed_systems_per_view.remove(view_id).unwrap_or_else(|| {
if cfg!(debug_assertions) {
re_log::warn_once!(
"Visualizers for view {:?} haven't been executed prior to display. This should never happen, please report a bug.",
view_blueprint.display_name_or_default()
);
}
let ctx: &'a ViewerContext<'_> = self.ctx;
let view = view_blueprint;
re_tracing::profile_scope!("late-system-execute", view.class_identifier().as_str());
let query_result = ctx.lookup_query_result(view.id);
let mut per_visualizer_data_results = re_viewer_context::PerSystemDataResults::default();
{
re_tracing::profile_scope!("per_system_data_results");
query_result.tree.visit(&mut |node| {
for system in &node.data_result.visualizers {
per_visualizer_data_results
.entry(*system)
.or_default()
.push(&node.data_result);
}
true
});
}
let class_registry = self.ctx.view_class_registry();
let class = view_blueprint.class(class_registry);
let context_system_once_per_frame_results = class_registry.run_once_per_frame_context_systems(
ctx,
std::iter::once(view.class_identifier())
);
execute_systems_for_view(ctx, view, self.view_states.get_mut_or_create(*view_id, class), &context_system_once_per_frame_results)
});
let class = view_blueprint.class(self.ctx.view_class_registry());
let view_state = self.view_states.get_mut_or_create(*view_id, class);
let response = ui.scope(|ui| {
class
.ui(self.ctx, ui, view_state, &query, system_output)
.unwrap_or_else(|err| {
re_log::error!(
"Error in view UI (class: {}, display name: {}): {err}",
view_blueprint.class_identifier(),
class.display_name(),
);
});
ui.ctx().memory_mut(|mem| {
mem.caches
.cache::<re_viewer_context::ViewRectPublisher>()
.set(
*view_id,
PublishedViewInfo {
name: view_blueprint.display_name_or_default().as_ref().to_owned(),
rect: ui.max_rect(),
},
);
});
});
response.response.widget_info(|| {
let mut info = egui::WidgetInfo::new(egui::WidgetType::Panel);
info.label = Some(view_blueprint.display_name_or_default().as_ref().to_owned());
info
});
Default::default()
}
fn tab_title_for_pane(&mut self, view_id: &ViewId) -> egui::WidgetText {
if let Some(view) = self.viewport_blueprint.view(view_id) {
view.display_name_or_default().as_ref().into()
} else {
re_log::warn_once!("ViewId missing during egui_tiles");
self.ctx.egui_ctx().error_text("Internal error").into()
}
}
fn tab_ui(
&mut self,
tiles: &mut egui_tiles::Tiles<ViewId>,
ui: &mut egui::Ui,
id: egui::Id,
tile_id: egui_tiles::TileId,
tab_state: &egui_tiles::TabState,
) -> egui::Response {
let mut tab_widget = TabWidget::new(self, ui, tiles, tile_id, tab_state, 1.0);
let response = ui
.interact(tab_widget.rect, id, egui::Sense::click_and_drag())
.on_hover_cursor(egui::CursorIcon::Grab);
let label = tab_widget.label.take();
response.widget_info(|| {
let mut info = egui::WidgetInfo::new(egui::WidgetType::Label);
info.label = label.clone();
info
});
if ui.is_rect_visible(tab_widget.rect) && !tab_state.is_being_dragged {
tab_widget.paint(ui);
}
let item = tiles.get(tile_id).and_then(|tile| match tile {
egui_tiles::Tile::Pane(view_id) => Some(Item::View(*view_id)),
egui_tiles::Tile::Container(_) => {
if let Some(Contents::Container(container_id)) =
self.contents_per_tile_id.get(&tile_id)
{
Some(Item::Container(*container_id))
} else {
None
}
}
});
if let Some(item) = item {
context_menu_ui_for_item(
self.ctx,
self.viewport_blueprint,
&item,
&response,
SelectionUpdateBehavior::OverrideSelection,
);
self.ctx
.handle_select_hover_drag_interactions(&response, item, false);
}
response
}
fn drag_ui(
&mut self,
tiles: &egui_tiles::Tiles<ViewId>,
ui: &mut egui::Ui,
tile_id: egui_tiles::TileId,
) {
let tab_widget = TabWidget::new(
self,
ui,
tiles,
tile_id,
&egui_tiles::TabState {
active: true,
is_being_dragged: true,
..Default::default()
},
0.5,
);
let frame = egui::Frame::NONE;
frame.show(ui, |ui| {
tab_widget.paint(ui);
});
}
fn retain_pane(&mut self, view_id: &ViewId) -> bool {
self.viewport_blueprint.views.contains_key(view_id)
}
fn top_bar_right_ui(
&mut self,
tiles: &egui_tiles::Tiles<ViewId>,
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(view_id) = active else {
return;
};
let view_id = *view_id;
let Some(view_blueprint) = self.viewport_blueprint.view(&view_id) else {
return;
};
let num_views = tiles.tiles().filter(|tile| tile.is_pane()).count();
ui.add_space(8.0);
if *self.maximized == Some(view_id) {
if ui
.small_icon_button(&re_ui::icons::MINIMIZE, "Restore viewport")
.on_hover_ui(|ui| {
Help::new_without_title()
.control(
"Restore - show all spaces",
IconText::from_keyboard_shortcut(ui.ctx().os(), TOGGLE_MAXIMIZE_VIEW),
)
.ui(ui);
})
.clicked()
|| ui.input_mut(|input| input.consume_shortcut(&TOGGLE_MAXIMIZE_VIEW))
{
*self.maximized = None;
MaximizeAnimationState::restore_view(ui.ctx(), view_id);
}
} else if num_views > 1 {
let is_view_the_only_selected =
self.ctx.selection().is_view_the_only_selected(&view_id);
let toggle = is_view_the_only_selected
&& ui.input_mut(|input| input.consume_shortcut(&TOGGLE_MAXIMIZE_VIEW));
if ui
.small_icon_button(&re_ui::icons::MAXIMIZE, "Maximize view")
.on_hover_ui(|ui| {
if is_view_the_only_selected {
Help::new_without_title()
.control(
"Maximize view",
IconText::from_keyboard_shortcut(
ui.ctx().os(),
TOGGLE_MAXIMIZE_VIEW,
),
)
.ui(ui);
} else {
ui.label("Maximize view");
}
})
.clicked()
|| toggle
{
*self.maximized = Some(view_id);
if let Some(rect) = tiles.rect(tile_id) {
MaximizeAnimationState::start_maximizing(ui.ctx(), view_id, rect);
}
}
}
if 1 < num_views {
let mut visible = true;
ui.visibility_toggle_button(&mut visible)
.on_hover_text("Hide this view");
if !visible {
self.viewport_blueprint.set_content_visibility(
self.ctx,
&Contents::View(view_id),
visible,
);
}
}
let view_class = view_blueprint.class(self.ctx.view_class_registry());
let view_state = self.view_states.get_mut_or_create(view_id, view_class);
view_class
.extra_title_bar_ui(
self.ctx,
ui,
view_state,
&view_blueprint.space_origin,
view_id,
)
.unwrap_or_else(|err| {
re_log::error!(
"Error in view title bar UI (class: {}, display name: {}): {err}",
view_blueprint.class_identifier(),
view_class.display_name(),
);
});
ui.help_button(|ui| {
view_class.help(ui.ctx().os()).ui(ui);
});
self.visualizer_errors_button(ui, view_id);
}
fn tab_bar_color(&self, visuals: &egui::Visuals) -> egui::Color32 {
let theme = if visuals.dark_mode {
egui::Theme::Dark
} else {
egui::Theme::Light
};
re_ui::design_tokens_of(theme).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 {
design_tokens_of_visuals(visuals).tile_drag_preview_stroke
}
fn drag_preview_color(&self, visuals: &egui::Visuals) -> egui::Color32 {
design_tokens_of_visuals(visuals).tile_drag_preview_color
}
fn tab_bar_height(&self, style: &egui::Style) -> f32 {
re_ui::design_tokens_of_visuals(&style.visuals).title_bar_height()
}
fn simplification_options(&self) -> egui_tiles::SimplificationOptions {
re_viewport_blueprint::tree_simplification_options()
}
fn on_edit(&mut self, edit_action: egui_tiles::EditAction) {
re_log::trace!("Tree edit: {edit_action:?}");
match edit_action {
EditAction::TileDropped => {
self.tile_dropped = true;
self.edited = true;
}
EditAction::TabSelected | EditAction::TileResized => {
self.edited = true;
}
EditAction::TileDragged => {
}
}
}
}
impl TilesDelegate<'_, '_> {
fn visualizer_errors_button(&self, ui: &mut egui::Ui, view_id: ViewId) {
let Some(visualizer_errors) = self.view_states.visualizer_errors(view_id) else {
return;
};
let errors = visualizer_errors
.values()
.flat_map(|err| match err {
re_viewer_context::VisualizerExecutionErrorState::Overall(error) => {
Either::Left(std::iter::once((Item::View(view_id), error.to_string())))
}
re_viewer_context::VisualizerExecutionErrorState::PerEntity(errors) => {
Either::Right(errors.iter().map(|(entity, err)| {
(
Item::DataResult(view_id, entity.clone().into()),
err.clone(),
)
}))
}
})
.collect::<Vec<_>>();
if visualizer_errors.is_empty() {
return;
}
let error_count = visualizer_errors.len();
ui.scope(|ui| {
let response = ui
.add(egui::Button::image(
icons::ERROR
.as_image()
.fit_to_exact_size(ui.tokens().small_icon_size)
.alt_text("View errors")
.tint(ui.visuals().error_fg_color),
))
.on_hover_text(format!(
"Show {error_count} visualizer error{}",
re_format::format_plural_s(error_count)
));
egui::Popup::menu(&response)
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| {
egui::ScrollArea::vertical()
.min_scrolled_height(600.0)
.max_height(600.0)
.show(ui, |ui| {
for (item, err) in errors {
self.show_item_error(ui, item, &err);
}
});
});
});
}
fn show_item_error(&self, ui: &mut egui::Ui, item: Item, err: &str) {
let item_entity = match &item {
Item::View(blueprint_id) => blueprint_id.as_entity_path().to_string(),
_ => item
.to_data_path()
.map(|item| item.to_string())
.unwrap_or_default(),
};
egui::Frame::window(ui.style())
.corner_radius(4)
.inner_margin(10.0)
.fill(ui.tokens().notification_panel_background_color)
.shadow(egui::Shadow::NONE)
.show(ui, |ui| {
ui.horizontal_top(|ui| {
ui.add(icons::ERROR.as_image().tint(ui.visuals().error_fg_color));
ui.vertical(|ui| {
if ui
.selectable_label(
self.ctx.selection().contains_item(&item),
item_entity,
)
.clicked()
{
self.ctx
.command_sender()
.send_system(SystemCommand::set_selection(item));
self.ctx
.command_sender()
.send_ui(re_ui::UICommand::ExpandSelectionPanel);
}
ui.separator();
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
ui.label(err);
})
})
});
}
}
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,
label: Option<String>,
}
impl TabWidget {
fn new<'a>(
tab_viewer: &'a mut TilesDelegate<'_, '_>,
ui: &'a mut egui::Ui,
tiles: &'a egui_tiles::Tiles<ViewId>,
tile_id: egui_tiles::TileId,
tab_state: &egui_tiles::TabState,
alpha: f32,
) -> Self {
let tokens = ui.tokens();
struct TabDesc {
widget_text: egui::WidgetText,
user_named: bool,
icon: &'static re_ui::Icon,
item: Option<Item>,
label: Option<String>,
}
let tab_desc = match tiles.get(tile_id) {
Some(egui_tiles::Tile::Pane(view_id)) => {
if let Some(view) = tab_viewer.viewport_blueprint.view(view_id) {
TabDesc {
widget_text: tab_viewer.tab_title_for_pane(view_id),
user_named: view.display_name.is_some(),
icon: view.class(tab_viewer.ctx.view_class_registry()).icon(),
item: Some(Item::View(*view_id)),
label: Some(view.display_name_or_default().into()),
}
} else {
re_log::warn_once!("View {view_id} not found");
TabDesc {
widget_text: tab_viewer.ctx.egui_ctx().error_text("Unknown view").into(),
icon: &re_ui::icons::VIEW_GENERIC,
user_named: false,
item: None,
label: None,
}
}
}
Some(egui_tiles::Tile::Container(container)) => {
if let Some(Contents::Container(container_id)) =
tab_viewer.contents_per_tile_id.get(&tile_id)
{
let (label, user_named) = if let Some(container_blueprint) =
tab_viewer.viewport_blueprint.container(container_id)
{
(
container_blueprint
.display_name_or_default()
.as_ref()
.into(),
container_blueprint.display_name.is_some(),
)
} else {
re_log::warn_once!("Container {container_id} missing during egui_tiles");
(
tab_viewer
.ctx
.egui_ctx()
.error_text("Internal error")
.into(),
false,
)
};
TabDesc {
widget_text: label,
user_named,
icon: icon_for_container_kind(&container.kind()),
item: Some(Item::Container(*container_id)),
label: None,
}
} else {
if container.kind() == egui_tiles::ContainerKind::Tabs
&& let Some(tile_id) = container.only_child()
{
return Self::new(tab_viewer, ui, tiles, tile_id, tab_state, alpha);
}
re_log::warn_once!("Container for tile ID {tile_id:?} not found");
TabDesc {
widget_text: tab_viewer
.ctx
.egui_ctx()
.error_text("Unknown container")
.into(),
icon: &re_ui::icons::VIEW_GENERIC,
user_named: false,
item: None,
label: None,
}
}
}
None => {
re_log::warn_once!("Tile {tile_id:?} not found");
TabDesc {
widget_text: tab_viewer
.ctx
.egui_ctx()
.error_text("Internal error")
.into(),
icon: &re_ui::icons::VIEW_UNKNOWN,
user_named: false,
item: None,
label: None,
}
}
};
let hovered = tab_desc
.item
.as_ref()
.is_some_and(|item| tab_viewer.ctx.hovered().contains_item(item));
let selected = tab_desc
.item
.as_ref()
.is_some_and(|item| tab_viewer.ctx.selection().contains_item(item));
let icon_size = tokens.small_icon_size;
let icon_width_plus_padding = icon_size.x + tokens.text_to_icon_padding();
let text = if tab_desc.user_named {
tab_desc.widget_text
} else {
tab_desc.widget_text.italics() };
let font_id = egui::TextStyle::Button.resolve(ui.style());
let galley = text.into_galley(ui, Some(egui::TextWrapMode::Extend), 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,
tokens.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 if tab_state.active {
ui.visuals().panel_fill
} else {
tab_viewer.tab_bar_color(ui.visuals())
};
let text_color = if selected {
if hovered {
ui.tokens().text_color_on_primary_hovered
} else {
ui.tokens().text_color_on_primary
}
} else {
tab_viewer.tab_text_color(ui.visuals(), tiles, tile_id, tab_state)
};
let bg_color = bg_color.gamma_multiply(alpha);
let text_color = text_color.gamma_multiply(alpha);
Self {
galley,
rect,
galley_rect,
icon: tab_desc.icon,
icon_size,
icon_rect,
bg_color,
text_color,
unnamed_style: !tab_desc.user_named,
label: tab_desc.label,
}
}
fn paint(self, ui: &egui::Ui) {
ui.painter().rect_filled(self.rect, 0.0, self.bg_color);
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,
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum MaximizeAnimationState {
#[default]
Nothing,
Maximizing {
view_id: ViewId,
start_time: web_time::Instant,
normal_rect: egui::Rect,
},
Restoring {
view_id: ViewId,
start_time: web_time::Instant,
normal_rect: egui::Rect,
},
}
impl MaximizeAnimationState {
fn start_maximizing(egui_ctx: &egui::Context, view_id: ViewId, rect: egui::Rect) {
egui_ctx.data_mut(|data| {
data.insert_temp(
egui::Id::NULL,
Self::Maximizing {
view_id,
normal_rect: rect,
start_time: web_time::Instant::now(),
},
);
});
egui_ctx.request_repaint();
}
fn restore_view(egui_ctx: &egui::Context, view_id: ViewId) {
egui_ctx.data_mut(|data| {
let animation_state = data.get_temp_mut_or_default(egui::Id::NULL);
if let Self::Maximizing {
view_id: animation_view_id,
normal_rect,
..
} = animation_state
&& view_id == *animation_view_id
{
*animation_state = Self::Restoring {
view_id,
start_time: web_time::Instant::now(),
normal_rect: *normal_rect,
};
}
});
egui_ctx.request_repaint();
}
fn animated_view_and_rect(
self,
egui_ctx: &egui::Context,
viewport_rect: egui::Rect,
) -> (Option<ViewId>, egui::Rect) {
let animation_time = egui_ctx.style().animation_time;
let mut animating_view_id = None;
let mut animated_rect = viewport_rect;
match self {
Self::Nothing => {}
Self::Maximizing {
view_id,
start_time,
normal_rect,
} => {
let progress = remap_clamp(
start_time.elapsed().as_secs_f32(),
0.0..=animation_time,
0.0..=1.0,
);
let progress = egui::emath::easing::quadratic_out(progress);
if progress < 1.0 {
egui_ctx.request_repaint();
animated_rect = normal_rect.lerp_towards(&viewport_rect, progress);
animating_view_id = Some(view_id);
} else {
}
}
Self::Restoring {
view_id,
start_time,
normal_rect,
} => {
let progress = remap_clamp(
start_time.elapsed().as_secs_f32(),
0.0..=animation_time,
0.0..=1.0,
);
let progress = egui::emath::easing::quadratic_out(progress); if progress < 1.0 {
egui_ctx.request_repaint();
animated_rect = viewport_rect.lerp_towards(&normal_rect, progress);
animating_view_id = Some(view_id);
}
}
}
let animated_rect = animated_rect.intersect(viewport_rect);
(animating_view_id, animated_rect)
}
}