Skip to content

Commit

Permalink
Support toggling item visibility on touch screens (#5624)
Browse files Browse the repository at this point in the history
### What
You can now click the "hover" buttons on list items on touch screens, by
first selecting the item, which will now make the buttons appear.

I also did a few other improvements.

**Best reviewed commit by commit**

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using newly built examples:
[app.rerun.io](https://app.rerun.io/pr/5624/index.html)
* Using examples from latest `main` build:
[app.rerun.io](https://app.rerun.io/pr/5624/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[app.rerun.io](https://app.rerun.io/pr/5624/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/5624)
- [Docs
preview](https://rerun.io/preview/0e0bee82f711f38ac23dcae16ffed87bf9cf96b0/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/0e0bee82f711f38ac23dcae16ffed87bf9cf96b0/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
emilk authored Mar 21, 2024
1 parent 9236a91 commit 4a1f6a4
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 64 deletions.
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

0 comments on commit 4a1f6a4

Please sign in to comment.