Skip to content

Commit

Permalink
Improve how the root container is displayed and handled in the bluepr…
Browse files Browse the repository at this point in the history
…int tree (#4989)

### What

This PR changes how the root container is displayed and handled:
- It's labeled as "Viewport", since it defines the top-level
organisation of the viewport.
- It's not collapsible, so it doesn't have a triangle. This reduces the
right-ward drift.
- It (still) cannot be dragged (but now the difference with other
containers is more obvious). It now behaves better when dragged over
though.
- It cannot be removed.

Other than that, it still behaves mostly as other containers (in
particular, it can be selected and edited).

Some changes were needed in the drag-and-drop code, making the
"if-statement-of-death" incrementally more complicated, sadly.

* Fixes #4909

<img width="221" alt="image"
src="https://github.com/rerun-io/rerun/assets/49431240/dc97a103-463d-4190-ba7c-03f6ddb502a4">


### 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/4989/index.html)
* Using examples from latest `main` build:
[app.rerun.io](https://app.rerun.io/pr/4989/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/4989/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

- [PR Build Summary](https://build.rerun.io/pr/4989)
- [Docs
preview](https://rerun.io/preview/f0e2e0f5a8b95c99a75fa09038187289ff3a3173/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/f0e2e0f5a8b95c99a75fa09038187289ff3a3173/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
abey79 authored Feb 1, 2024
1 parent 4897b93 commit a1a4df2
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 116 deletions.
14 changes: 11 additions & 3 deletions crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,9 +1153,17 @@ mod hierarchical_drag_and_drop {

let item_desc = re_ui::drag_and_drop::ItemContext {
id: item_id,
is_container,
parent_id,
position_index_in_parent,
item_kind: if is_container {
re_ui::drag_and_drop::ItemKind::Container {
parent_id,
position_index_in_parent,
}
} else {
re_ui::drag_and_drop::ItemKind::Leaf {
parent_id,
position_index_in_parent,
}
},
previous_container_id,
};

Expand Down
260 changes: 158 additions & 102 deletions crates/re_ui/src/drag_and_drop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,35 @@
//!
//! Works well in combination with [`crate::list_item::ListItem`].
pub enum ItemKind<ItemId: Copy> {
/// Root container item.
///
/// Root container don't have a parent and are restricted when hovered as drop target (the dragged item can only go
/// _in_ the root container, not before or after).
RootContainer,

/// Container item.
Container {
parent_id: ItemId,
position_index_in_parent: usize,
},

/// Leaf item.
Leaf {
parent_id: ItemId,
position_index_in_parent: usize,
},
}

/// Context information about the hovered item.
///
/// This is used by [`find_drop_target`] to compute the [`DropTarget`], if any.
pub struct ItemContext<ItemId: Copy> {
/// ID of the item being hovered during drag
pub id: ItemId,

/// Can this item "contain" the currently dragged item?
pub is_container: bool,

/// ID of the parent of this item.
pub parent_id: ItemId,

/// Position of this item within its parent.
pub position_index_in_parent: usize,
/// What kind of item is this?
pub item_kind: ItemKind<ItemId>,

/// ID of the container just before this item within the parent, if such a container exists.
pub previous_container_id: Option<ItemId>,
Expand Down Expand Up @@ -137,11 +151,23 @@ impl<ItemId: Copy> DropTarget<ItemId> {
/// ┌─▼─── ║ ║ │
/// │ ║ ║ │
/// └───── ╚═════════════════════════════════════════════╝ ─┘
///
/// not a valid drop zone
/// │
/// ╔═════════════════════════════════▼══════════════════╗
/// root container ║ ║
/// item ║ ────────────────────────────────────────────────── ║
/// ║ ║
/// ╚════════════════════════▲═══════════════════════════╝
/// │
/// insert inside me
/// at pos = 0
/// ```
///
/// Here are a few observations of the above that help navigate the "if-statement-of-death"
/// in the implementation:
/// - The top parts of the item are treated the same in all four cases.
/// - The top parts of the item are treated the same in most cases (root container is an
/// exception).
/// - Handling of the body can be simplified by making the sensitive area either a small
/// corner (container case), or the entire body (leaf case). Then, that area always maps
/// to "insert after me".
Expand All @@ -158,20 +184,25 @@ pub fn find_drop_target<ItemId: Copy>(
) -> Option<DropTarget<ItemId>> {
let indent = ui.spacing().indent;
let item_id = item_context.id;
let is_container = item_context.is_container;
let parent_id = item_context.parent_id;
let pos_in_parent = item_context.position_index_in_parent;
let item_kind = &item_context.item_kind;
let is_non_root_container = matches!(item_kind, ItemKind::Container { .. });

// For both leaf and containers we have two drag zones on the upper half of the item.
let (top, mut bottom) = item_rect.split_top_bottom_at_fraction(0.5);
let (left_top, top) = top.split_left_right_at_x(top.left() + indent);
let (mut top, mut bottom) = item_rect.split_top_bottom_at_fraction(0.5);

let mut left_top = egui::Rect::NOTHING;
if matches!(item_kind, ItemKind::RootContainer) {
top = egui::Rect::NOTHING;
} else {
(left_top, top) = top.split_left_right_at_x(top.left() + indent);
}

// For the lower part of the item, the story is more complicated:
// - for leaf item, we have a single drag zone on the entire lower half
// - for leaf and root container items, we have a single drag zone on the entire lower half
// - for container item, we must distinguish between the indent part and the rest, plus check some area in the
// body
let mut left_bottom = egui::Rect::NOTHING;
if is_container {
if is_non_root_container {
(left_bottom, bottom) = bottom.split_left_right_at_x(bottom.left() + indent);
}

Expand All @@ -181,7 +212,7 @@ pub fn find_drop_target<ItemId: Copy>(
// - leaf item: the entire body area, if any, cannot receive a drag (by definition) and thus homogeneously maps
// to "insert after me"
let body_insert_after_me_area = if let Some(body_rect) = body_rect {
if item_context.is_container {
if is_non_root_container {
egui::Rect::from_two_pos(
body_rect.left_bottom() + egui::vec2(indent, -item_height / 2.0),
body_rect.left_bottom(),
Expand Down Expand Up @@ -226,95 +257,120 @@ pub fn find_drop_target<ItemId: Copy>(
}
}

/* ===== TOP SECTIONS (same leaf/container items) ==== */
if ui.rect_contains_pointer(left_top) {
// insert before me
Some(DropTarget::new(
item_rect.x_range(),
top.top(),
match *item_kind {
ItemKind::RootContainer => {
// we just need to test the bottom section in this case.
if ui.rect_contains_pointer(bottom) {
Some(DropTarget::new(
item_rect.x_range(),
bottom.bottom(),
item_id,
0,
))
} else {
None
}
}

ItemKind::Container {
parent_id,
pos_in_parent,
))
} else if ui.rect_contains_pointer(top) {
// insert last in the previous container if any, else insert before me
if let Some(previous_container_id) = item_context.previous_container_id {
Some(DropTarget::new(
(item_rect.left() + indent..=item_rect.right()).into(),
top.top(),
previous_container_id,
usize::MAX,
))
} else {
Some(DropTarget::new(
item_rect.x_range(),
top.top(),
parent_id,
pos_in_parent,
))
position_index_in_parent,
}
}
/* ==== BODY SENSE AREA ==== */
else if ui.rect_contains_pointer(body_insert_after_me_area) {
// insert after me in my parent
Some(DropTarget::new(
item_rect.x_range(),
body_insert_after_me_area.bottom(),
| ItemKind::Leaf {
parent_id,
pos_in_parent + 1,
))
}
/* ==== BOTTOM SECTIONS (leaf item) ==== */
else if !is_container {
if ui.rect_contains_pointer(bottom) {
let position_y = if let Some(body_rect) = non_empty_body_rect {
body_rect.bottom()
} else {
bottom.bottom()
};
position_index_in_parent,
} => {
/* ===== TOP SECTIONS (same leaf/container items) ==== */
if ui.rect_contains_pointer(left_top) {
// insert before me
Some(DropTarget::new(
item_rect.x_range(),
top.top(),
parent_id,
position_index_in_parent,
))
} else if ui.rect_contains_pointer(top) {
// insert last in the previous container if any, else insert before me
if let Some(previous_container_id) = item_context.previous_container_id {
Some(DropTarget::new(
(item_rect.left() + indent..=item_rect.right()).into(),
top.top(),
previous_container_id,
usize::MAX,
))
} else {
Some(DropTarget::new(
item_rect.x_range(),
top.top(),
parent_id,
position_index_in_parent,
))
}
}
/* ==== BODY SENSE AREA ==== */
else if ui.rect_contains_pointer(body_insert_after_me_area) {
// insert after me in my parent
Some(DropTarget::new(
item_rect.x_range(),
body_insert_after_me_area.bottom(),
parent_id,
position_index_in_parent + 1,
))
}
/* ==== BOTTOM SECTIONS (leaf item) ==== */
else if !is_non_root_container {
if ui.rect_contains_pointer(bottom) {
let position_y = if let Some(body_rect) = non_empty_body_rect {
body_rect.bottom()
} else {
bottom.bottom()
};

// insert after me
Some(DropTarget::new(
item_rect.x_range(),
position_y,
parent_id,
pos_in_parent + 1,
))
} else {
None
}
}
/* ==== BOTTOM SECTIONS (container item) ==== */
else if let Some(body_rect) = non_empty_body_rect {
if ui.rect_contains_pointer(left_bottom) || ui.rect_contains_pointer(bottom) {
// insert at pos = 0 inside me
Some(DropTarget::new(
(body_rect.left() + indent..=body_rect.right()).into(),
left_bottom.bottom(),
item_id,
0,
))
} else {
None
// insert after me
Some(DropTarget::new(
item_rect.x_range(),
position_y,
parent_id,
position_index_in_parent + 1,
))
} else {
None
}
}
/* ==== BOTTOM SECTIONS (container item) ==== */
else if let Some(body_rect) = non_empty_body_rect {
if ui.rect_contains_pointer(left_bottom) || ui.rect_contains_pointer(bottom) {
// insert at pos = 0 inside me
Some(DropTarget::new(
(body_rect.left() + indent..=body_rect.right()).into(),
left_bottom.bottom(),
item_id,
0,
))
} else {
None
}
} else if ui.rect_contains_pointer(left_bottom) {
// insert after me in my parent
Some(DropTarget::new(
item_rect.x_range(),
left_bottom.bottom(),
parent_id,
position_index_in_parent + 1,
))
} else if ui.rect_contains_pointer(bottom) {
// insert at pos = 0 inside me
Some(DropTarget::new(
(item_rect.left() + indent..=item_rect.right()).into(),
bottom.bottom(),
item_id,
0,
))
}
/* ==== Who knows where else the mouse cursor might wander… ¯\_(ツ)_/¯ ==== */
else {
None
}
}
} else if ui.rect_contains_pointer(left_bottom) {
// insert after me in my parent
Some(DropTarget::new(
item_rect.x_range(),
left_bottom.bottom(),
parent_id,
pos_in_parent + 1,
))
} else if ui.rect_contains_pointer(bottom) {
// insert at pos = 0 inside me
Some(DropTarget::new(
(item_rect.left() + indent..=item_rect.right()).into(),
bottom.bottom(),
item_id,
0,
))
}
/* ==== Who knows where else the mouse cursor might wander… ¯\_(ツ)_/¯ ==== */
else {
None
}
}
Loading

0 comments on commit a1a4df2

Please sign in to comment.