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

Support toggling item visibility on touch screens #5624

Merged
merged 6 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ impl eframe::App for ExampleApp {
let mut item = re_ui
.list_item(label)
.selected(Some(i) == self.selected_list_item)
.active(i != 3)
.interactive(i != 3)
.with_buttons(|re_ui, ui| {
re_ui.small_icon_button(ui, &re_ui::icons::ADD)
| re_ui.small_icon_button(ui, &re_ui::icons::REMOVE)
Expand Down
135 changes: 75 additions & 60 deletions crates/re_ui/src/list_item.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{Icon, LabelStyle, ReUi};
use egui::epaint::text::TextWrapping;
use egui::{epaint::text::TextWrapping, WidgetText};
use egui::{Align, Align2, Response, Shape, Ui};
use std::default::Default;

Expand Down Expand Up @@ -111,7 +111,7 @@ pub enum WidthAllocationMode {
pub struct ListItem<'a> {
text: egui::WidgetText,
re_ui: &'a ReUi,
active: bool,
interactive: bool,
selected: bool,
draggable: bool,
drag_target: bool,
Expand All @@ -133,7 +133,7 @@ impl<'a> ListItem<'a> {
Self {
text: text.into(),
re_ui,
active: true,
interactive: true,
selected: false,
draggable: false,
drag_target: false,
Expand All @@ -150,10 +150,12 @@ impl<'a> ListItem<'a> {
}
}

/// Set the active state the item.
/// Can the user click and interact with it?
///
/// Set to `false` for items that only show info, but shouldn't be interactive.
#[inline]
pub fn active(mut self, active: bool) -> Self {
self.active = active;
pub fn interactive(mut self, interactive: bool) -> Self {
self.interactive = interactive;
self
}

Expand Down Expand Up @@ -272,6 +274,8 @@ impl<'a> ListItem<'a> {

/// Provide a closure to display on-hover buttons on the right of the item.
///
/// Buttons also show when the item is selected, in order to support clicking them on touch screens.
///
/// Notes:
/// - If buttons are used, the item will allocate the full available width of the parent. If the
/// enclosing UI adapts to the childrens width, it will unnecessarily grow. If buttons aren't
Expand Down Expand Up @@ -352,42 +356,59 @@ impl<'a> ListItem<'a> {
}
}

fn ui(mut self, ui: &mut Ui, id: Option<egui::Id>) -> ListItemResponse {
let collapse_extra = if self.collapse_openness.is_some() {
fn ui(self, ui: &mut Ui, id: Option<egui::Id>) -> ListItemResponse {
let Self {
mut text,
re_ui,
interactive,
selected,
draggable,
drag_target,
subdued,
weak,
mut italics,
label_style,
force_hovered,
collapse_openness,
height,
width_allocation_mode,
icon_fn,
buttons_fn,
} = self;

let collapse_extra = if collapse_openness.is_some() {
ReUi::collapsing_triangle_area().x + ReUi::text_to_icon_padding()
} else {
0.0
};
let icon_extra = if self.icon_fn.is_some() {
let icon_extra = if icon_fn.is_some() {
ReUi::small_icon_size().x + ReUi::text_to_icon_padding()
} else {
0.0
};

match self.label_style {
match label_style {
LabelStyle::Normal => {}
LabelStyle::Unnamed => {
self.italics = true;
italics = true;
}
}

if self.italics {
self.text = self.text.italics();
if italics {
text = text.italics();
}

/// Compute the "ideal" desired width of the item, accounting for text and icon(s) (but not
/// buttons).
fn icons_and_label_width(
ui: &egui::Ui,
item: &ListItem<'_>,
text: &WidgetText,
collapse_extra: f32,
icon_extra: f32,
) -> f32 {
let layout_job = item.text.clone().into_layout_job(
ui.style(),
egui::FontSelection::Default,
Align::LEFT,
);
let layout_job =
text.clone()
.into_layout_job(ui.style(), egui::FontSelection::Default, Align::LEFT);
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));

let text_width = galley.size().x;
Expand All @@ -397,22 +418,23 @@ impl<'a> ListItem<'a> {
(collapse_extra + icon_extra + text_width).ceil()
}

let desired_width = match self.width_allocation_mode {
let desired_width = match width_allocation_mode {
WidthAllocationMode::Available => ui.available_width(),
WidthAllocationMode::Compact => {
icons_and_label_width(ui, &self, collapse_extra, icon_extra)
icons_and_label_width(ui, &text, collapse_extra, icon_extra)
}
};

let desired_size = egui::vec2(desired_width, self.height);
let (rect, mut response) = ui.allocate_at_least(
desired_size,
if self.draggable {
egui::Sense::click_and_drag()
} else {
egui::Sense::click()
},
);
let desired_size = egui::vec2(desired_width, height);

let sense = if !interactive {
egui::Sense::hover()
} else if draggable {
egui::Sense::click_and_drag()
} else {
egui::Sense::click()
};
let (rect, mut response) = ui.allocate_at_least(desired_size, sense);

// compute the full-span background rect
let mut bg_rect = rect;
Expand All @@ -421,39 +443,34 @@ impl<'a> ListItem<'a> {

// we want to be able to select/hover the item across its full span, so we sense that and
// update the response accordingly.
let full_span_response = ui.interact(bg_rect, response.id, egui::Sense::click());
let full_span_response = ui.interact(bg_rect, response.id, sense);
response.clicked = full_span_response.clicked;
response.contains_pointer = full_span_response.contains_pointer;
response.hovered = full_span_response.hovered;

// override_hover should not affect the returned response
let mut style_response = response.clone();
if self.force_hovered {
if force_hovered {
style_response.contains_pointer = true;
style_response.hovered = true;
}

let mut collapse_response = None;

if ui.is_rect_visible(bg_rect) {
let mut visuals = if self.active {
ui.style()
.interact_selectable(&style_response, self.selected)
} else {
ui.visuals().widgets.inactive
};
let mut visuals = ui.style().interact_selectable(&style_response, selected);

// TODO(ab): use design tokens instead
if self.weak {
if weak {
visuals.fg_stroke.color = ui.visuals().weak_text_color();
} else if self.subdued {
} else if subdued {
visuals.fg_stroke.color = visuals.fg_stroke.color.gamma_multiply(0.5);
}

let background_frame = ui.painter().add(egui::Shape::Noop);

// Draw collapsing triangle
if let Some(openness) = self.collapse_openness {
if let Some(openness) = collapse_openness {
let triangle_pos = ui.painter().round_pos_to_pixels(egui::pos2(
rect.min.x,
rect.center().y - 0.5 * ReUi::collapsing_triangle_area().y,
Expand All @@ -470,28 +487,27 @@ impl<'a> ListItem<'a> {
}

// Draw icon
if let Some(icon_fn) = self.icon_fn {
if let Some(icon_fn) = icon_fn {
let icon_pos = ui.painter().round_pos_to_pixels(egui::pos2(
rect.min.x + collapse_extra,
rect.center().y - 0.5 * ReUi::small_icon_size().y,
));
let icon_rect = egui::Rect::from_min_size(icon_pos, ReUi::small_icon_size());
icon_fn(self.re_ui, ui, icon_rect, visuals);
icon_fn(re_ui, ui, icon_rect, visuals);
}

// Handle buttons
// Note: We should be able to just use `response.hovered()` here, which only returns `true` if no drag is in
// progress. Due to the response merging we do above, this breaks though. This is why we do an explicit
// rectangle and drag payload check.
//TODO(ab): refactor responses to address that.
let should_show_buttons = self.active
&& ui.rect_contains_pointer(rect)
&& !egui::DragAndDrop::has_any_payload(ui.ctx());
// We can't use `.hovered()` or the buttons disappear just as the user clicks,
// so we use `contains_pointer` instead. That also means we need to check
// that we aren't dragging anything.
let should_show_buttons = interactive
&& full_span_response.contains_pointer()
&& !egui::DragAndDrop::has_any_payload(ui.ctx())
|| selected; // by showing the buttons when selected, we allow users to find them on touch screens
let button_response = if should_show_buttons {
if let Some(buttons) = self.buttons_fn {
if let Some(buttons) = buttons_fn {
let mut ui =
ui.child_ui(rect, egui::Layout::right_to_left(egui::Align::Center));
Some(buttons(self.re_ui, &mut ui))
Some(buttons(re_ui, &mut ui))
} else {
None
}
Expand All @@ -506,16 +522,15 @@ impl<'a> ListItem<'a> {
text_rect.max.x -= button_response.rect.width() + ReUi::text_to_icon_padding();
}

match self.label_style {
match label_style {
LabelStyle::Normal => {}
LabelStyle::Unnamed => {
self.text = self.text.color(visuals.fg_stroke.color.gamma_multiply(0.5));
text = text.color(visuals.fg_stroke.color.gamma_multiply(0.5));
}
}

let mut layout_job =
self.text
.into_layout_job(ui.style(), egui::FontSelection::Default, Align::LEFT);
text.into_layout_job(ui.style(), egui::FontSelection::Default, Align::LEFT);
layout_job.wrap = TextWrapping::truncate_at_width(text_rect.width());

let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
Expand All @@ -524,7 +539,7 @@ impl<'a> ListItem<'a> {
response.widget_info(|| {
egui::WidgetInfo::selected(
egui::WidgetType::SelectableLabel,
self.selected,
selected,
galley.text(),
)
});
Expand All @@ -536,15 +551,15 @@ impl<'a> ListItem<'a> {
ui.painter().galley(text_pos, galley, visuals.text_color());

// Draw background on interaction.
if self.drag_target {
if drag_target {
ui.painter().set(
background_frame,
Shape::rect_stroke(bg_rect, 0.0, (1.0, ui.visuals().selection.bg_fill)),
);
} else {
let bg_fill = if button_response.map_or(false, |r| r.hovered()) {
Some(visuals.bg_fill)
} else if self.selected
} else if selected
|| style_response.hovered()
|| style_response.highlighted()
|| style_response.has_focus()
Expand Down
2 changes: 1 addition & 1 deletion crates/re_viewer/src/ui/recordings_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ fn recording_list_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui) -> bool {
} else {
ctx.re_ui
.list_item(app_id)
.active(false)
.interactive(false)
.show_hierarchical_with_content(
ui,
ui.make_persistent_id(app_id),
Expand Down
2 changes: 1 addition & 1 deletion crates/re_viewer/src/ui/selection_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ fn container_children(
ListItem::new(ctx.re_ui, "empty — use the + button to add content")
.weak(true)
.italics(true)
.active(false)
.interactive(false)
.show_flat(ui);
}
};
Expand Down
2 changes: 1 addition & 1 deletion crates/re_viewer/src/ui/space_view_space_origin_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ fn space_view_space_origin_widget_editing_ui(
)
.weak(true)
.italics(true)
.active(false)
.interactive(false)
.show_flat(ui);
}
};
Expand Down
Loading