Skip to content
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
33 changes: 33 additions & 0 deletions crates/gpui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,39 @@ The features on `gpui_platform` are platform-specific, so the list above is a sa

- **Windows** — no features are required. Windowing uses Win32 and text uses DirectWrite. `font-kit` has no effect here.

### Pan and zoom

`ElementTransform` applies translation and scale to a `div` subtree after layout.
Children keep their layout coordinates. Paint, clipping and hitboxes move together.

```rust
div().transform(
ElementTransform::default()
.origin(point(0.5, 0.5))
.translate(point(px(40.), px(10.)))
.scale(size(2., 2.)),
)
```

**Custom elements** must convert coordinates when they compare mouse input with
layout bounds, report automation bounds, or return IME rectangles. Capture
`window.element_transform()` during prepaint or paint and retain it for later
callbacks. Its translation uses logical pixels, independent of display density.

```rust
let transform = window.element_transform(); // capture while drawing
let screen_bounds = transform.apply_bounds(layout_bounds);
// Later, in a mouse callback, after the drawing scope has ended:
let local_position = transform.inverse().map(|inverse| inverse.apply(event.position));
```

`inverse()` returns `None` for singular or non-finite transforms, including zero
scale. Mouse event positions themselves remain in window coordinates.
Only `div` and text elements apply their own `Style::transform`; other elements
can be transformed through a parent `div`. Keep anchored overlays outside a
transformed subtree. Uniform positive scale also scales decoration geometry;
non-uniform scale leaves corner radii, border widths and shadow blur unchanged.

### Additional Topics

- [Ownership and data flow](_ownership_and_data_flow)
Expand Down
184 changes: 97 additions & 87 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2309,21 +2309,23 @@ impl Interactivity {
}

window.with_text_style(style.text_style().cloned(), |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
let hitbox = if self.should_insert_hitbox(&style, window, cx) {
Some(window.insert_hitbox(bounds, self.hitbox_behavior))
} else {
None
};
window.with_style_transform(style.transform, bounds, |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
let hitbox = if self.should_insert_hitbox(&style, window, cx) {
Some(window.insert_hitbox(bounds, self.hitbox_behavior))
} else {
None
};

let scroll_offset =
self.clamp_scroll_position(bounds, &style, window, cx);
let result = f(&style, scroll_offset, hitbox, window, cx);
(result, element_state)
},
)
let scroll_offset =
self.clamp_scroll_position(bounds, &style, window, cx);
let result = f(&style, scroll_offset, hitbox, window, cx);
(result, element_state)
},
)
})
})
},
)
Expand Down Expand Up @@ -2470,93 +2472,101 @@ impl Interactivity {
}

window.with_element_opacity(style.opacity, |window| {
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
window.with_text_style(style.text_style().cloned(), |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
window.with_tab_group(tab_group, |window| {
// Register the container's own focus handle *inside* its
// tab group, so that focusing the container and then
// calling `focus_next` descends into this group's first
// item. Inserting it before `with_tab_group` would give the
// container a shallower tab path than its children; with
// sibling groups every container would then sort ahead of
// every item, and `focus_next` from a container would jump
// to the first item in the whole window instead of its own.
if let Some(focus_handle) = &self.tracked_focus_handle {
window.next_frame.tab_stops.insert(focus_handle);
}
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(
global_id, hitbox, &style, window, cx,
);

if let Some(drag) = cx.active_drag.as_ref() {
if let Some(mouse_cursor) = drag.cursor_style {
window.set_window_cursor_style(mouse_cursor);
}
} else {
if let Some(mouse_cursor) = style.mouse_cursor {
window.with_style_transform(style.transform, bounds, |window| {
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
window.with_text_style(style.text_style().cloned(), |window| {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
window.with_tab_group(tab_group, |window| {
// Register the container's own focus handle *inside* its
// tab group, so that focusing the container and then
// calling `focus_next` descends into this group's first
// item. Inserting it before `with_tab_group` would give the
// container a shallower tab path than its children; with
// sibling groups every container would then sort ahead of
// every item, and `focus_next` from a container would jump
// to the first item in the whole window instead of its own.
if let Some(focus_handle) = &self.tracked_focus_handle {
window.next_frame.tab_stops.insert(focus_handle);
}
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(
global_id, hitbox, &style, window, cx,
);

if let Some(drag) = cx.active_drag.as_ref() {
if let Some(mouse_cursor) = drag.cursor_style {
window
.set_window_cursor_style(mouse_cursor);
}
} else if let Some(mouse_cursor) =
style.mouse_cursor
{
window.set_cursor_style(mouse_cursor, hitbox);
}
}

if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}
if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}

if let Some(area) = self.window_control {
window.insert_window_control_hitbox(
area,
hitbox.clone(),
if let Some(area) = self.window_control {
window.insert_window_control_hitbox(
area,
hitbox.clone(),
);
}

self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
window,
cx,
);
self.paint_scroll_listener(
hitbox, &style, window, cx,
);
}

self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
window,
cx,
);
self.paint_scroll_listener(hitbox, &style, window, cx);
}

self.paint_keyboard_listeners(window, cx);

if window.a11y.is_active() {
if let Some(global_id) = global_id {
if !self.a11y_action_listeners.is_empty() {
let node_id = global_id.accesskit_node_id();
for (action, listener) in
self.a11y_action_listeners.drain(..)
{
window.on_a11y_action(
node_id, action, listener,
);
self.paint_keyboard_listeners(window, cx);

if window.a11y.is_active() {
if let Some(global_id) = global_id {
if !self.a11y_action_listeners.is_empty() {
let node_id = global_id.accesskit_node_id();
for (action, listener) in
self.a11y_action_listeners.drain(..)
{
window.on_a11y_action(
node_id, action, listener,
);
}
}
}
}
}

f(&style, window, cx);
f(&style, window, cx);

if let Some(_hitbox) = hitbox {
#[cfg(any(feature = "inspector", debug_assertions))]
window.insert_inspector_hitbox(
_hitbox.id,
_inspector_id,
cx,
);
if let Some(_hitbox) = hitbox {
#[cfg(any(
feature = "inspector",
debug_assertions
))]
window.insert_inspector_hitbox(
_hitbox.id,
_inspector_id,
cx,
);

if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
}
}
}
})
},
);
})
},
);
});
});
});
});
Expand Down
Loading