Reviewed-on: #18 Co-authored-by: Ren Amamiya <reya@lume.nu> Co-committed-by: Ren Amamiya <reya@lume.nu>
20 KiB
20 KiB
Advanced Element Patterns
Advanced techniques and patterns for implementing sophisticated GPUI elements.
Custom Layout Algorithms
Implementing custom layout algorithms not supported by GPUI's built-in layouts.
Masonry Layout (Pinterest-Style)
pub struct MasonryLayout {
id: ElementId,
columns: usize,
gap: Pixels,
children: Vec<AnyElement>,
}
struct MasonryLayoutState {
column_layouts: Vec<Vec<LayoutId>>,
column_heights: Vec<Pixels>,
}
struct MasonryPaintState {
child_bounds: Vec<Bounds<Pixels>>,
}
impl Element for MasonryLayout {
type RequestLayoutState = MasonryLayoutState;
type PrepaintState = MasonryPaintState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, MasonryLayoutState) {
// Initialize columns
let mut columns: Vec<Vec<LayoutId>> = vec![Vec::new(); self.columns];
let mut column_heights = vec![px(0.); self.columns];
// Distribute children across columns
for child in &mut self.children {
let (child_layout_id, _) = child.request_layout(
global_id,
inspector_id,
window,
cx
);
let child_size = window.layout_bounds(child_layout_id).size;
// Find shortest column
let min_column_idx = column_heights
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap()
.0;
// Add child to shortest column
columns[min_column_idx].push(child_layout_id);
column_heights[min_column_idx] += child_size.height + self.gap;
}
// Calculate total layout size
let column_width = px(200.); // Fixed column width
let total_width = column_width * self.columns as f32
+ self.gap * (self.columns - 1) as f32;
let total_height = column_heights.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.copied()
.unwrap_or(px(0.));
let layout_id = window.request_layout(
Style {
size: size(total_width, total_height),
..default()
},
columns.iter().flatten().copied().collect(),
cx
);
(layout_id, MasonryLayoutState {
column_layouts: columns,
column_heights,
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut MasonryLayoutState,
window: &mut Window,
cx: &mut App
) -> MasonryPaintState {
let column_width = px(200.);
let mut child_bounds = Vec::new();
// Position children in columns
for (col_idx, column) in layout_state.column_layouts.iter().enumerate() {
let x_offset = bounds.left()
+ (column_width + self.gap) * col_idx as f32;
let mut y_offset = bounds.top();
for (child_idx, layout_id) in column.iter().enumerate() {
let child_size = window.layout_bounds(*layout_id).size;
let child_bound = Bounds::new(
point(x_offset, y_offset),
size(column_width, child_size.height)
);
self.children[child_idx].prepaint(
global_id,
inspector_id,
child_bound,
window,
cx
);
child_bounds.push(child_bound);
y_offset += child_size.height + self.gap;
}
}
MasonryPaintState { child_bounds }
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_state: &mut MasonryLayoutState,
paint_state: &mut MasonryPaintState,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(&paint_state.child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}
Circular Layout
pub struct CircularLayout {
id: ElementId,
radius: Pixels,
children: Vec<AnyElement>,
}
impl Element for CircularLayout {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Vec<LayoutId>) {
let child_layouts: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(global_id, inspector_id, window, cx).0)
.collect();
let diameter = self.radius * 2.;
let layout_id = window.request_layout(
Style {
size: size(diameter, diameter),
..default()
},
child_layouts.clone(),
cx
);
(layout_id, child_layouts)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_ids: &mut Vec<LayoutId>,
window: &mut Window,
cx: &mut App
) -> Vec<Bounds<Pixels>> {
let center = bounds.center();
let angle_step = 2.0 * std::f32::consts::PI / self.children.len() as f32;
let mut child_bounds = Vec::new();
for (i, (child, layout_id)) in self.children.iter_mut()
.zip(layout_ids.iter())
.enumerate()
{
let angle = angle_step * i as f32;
let child_size = window.layout_bounds(*layout_id).size;
// Position child on circle
let x = center.x + self.radius * angle.cos() - child_size.width / 2.;
let y = center.y + self.radius * angle.sin() - child_size.height / 2.;
let child_bound = Bounds::new(point(x, y), child_size);
child.prepaint(global_id, inspector_id, child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_ids: &mut Vec<LayoutId>,
child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}
Element Composition with Traits
Create reusable behaviors via traits for element composition.
Hoverable Trait
pub trait Hoverable: Element {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
}
// Implementation for custom element
pub struct HoverableElement {
id: ElementId,
content: AnyElement,
hover_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
hover_end_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
was_hovered: bool,
}
impl Hoverable for HoverableElement {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_handlers.push(Box::new(f));
self
}
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_end_handlers.push(Box::new(f));
self
}
}
impl Element for HoverableElement {
type RequestLayoutState = LayoutId;
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut LayoutId,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
let is_hovered = hitbox.is_hovered(window);
// Trigger hover events
if is_hovered && !self.was_hovered {
for handler in &self.hover_handlers {
handler(window, cx);
}
} else if !is_hovered && self.was_hovered {
for handler in &self.hover_end_handlers {
handler(window, cx);
}
}
self.was_hovered = is_hovered;
// Paint content
self.content.paint(bounds, window, cx);
}
// ... other methods
}
Clickable Trait
pub trait Clickable: Element {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
}
pub struct ClickableElement {
id: ElementId,
content: AnyElement,
click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
double_click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
last_click_time: Option<Instant>,
}
impl Clickable for ClickableElement {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.click_handlers.push(Box::new(f));
self
}
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.double_click_handlers.push(Box::new(f));
self
}
}
Async Element Updates
Elements that update based on async operations.
pub struct AsyncElement {
id: ElementId,
state: Entity<AsyncState>,
loading: bool,
data: Option<String>,
}
pub struct AsyncState {
loading: bool,
data: Option<String>,
}
impl Element for AsyncElement {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut (),
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Display loading or data
if self.loading {
// Paint loading indicator
self.paint_loading(bounds, window, cx);
} else if let Some(data) = &self.data {
// Paint data
self.paint_data(data, bounds, window, cx);
}
// Trigger async update on click
window.on_mouse_event({
let state = self.state.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Spawn async task
cx.spawn({
let state = state.clone();
async move {
// Perform async operation
let result = fetch_data_async().await;
// Update state on completion
state.update(cx, |state, cx| {
state.loading = false;
state.data = Some(result);
cx.notify();
});
}
}).detach();
// Set loading state immediately
state.update(cx, |state, cx| {
state.loading = true;
cx.notify();
});
cx.stop_propagation();
}
}
});
}
// ... other methods
}
async fn fetch_data_async() -> String {
// Simulate async operation
tokio::time::sleep(Duration::from_secs(1)).await;
"Data loaded!".to_string()
}
Element Memoization
Optimize performance by memoizing expensive element computations.
pub struct MemoizedElement<T: PartialEq + Clone + 'static> {
id: ElementId,
value: T,
render_fn: Box<dyn Fn(&T) -> AnyElement>,
cached_element: Option<AnyElement>,
last_value: Option<T>,
}
impl<T: PartialEq + Clone + 'static> MemoizedElement<T> {
pub fn new<F>(id: ElementId, value: T, render_fn: F) -> Self
where
F: Fn(&T) -> AnyElement + 'static,
{
Self {
id,
value,
render_fn: Box::new(render_fn),
cached_element: None,
last_value: None,
}
}
}
impl<T: PartialEq + Clone + 'static> Element for MemoizedElement<T> {
type RequestLayoutState = LayoutId;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, LayoutId) {
// Check if value changed
if self.last_value.as_ref() != Some(&self.value) || self.cached_element.is_none() {
// Recompute element
self.cached_element = Some((self.render_fn)(&self.value));
self.last_value = Some(self.value.clone());
}
// Request layout for cached element
let (layout_id, _) = self.cached_element
.as_mut()
.unwrap()
.request_layout(global_id, inspector_id, window, cx);
(layout_id, layout_id)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
window: &mut Window,
cx: &mut App
) -> () {
self.cached_element
.as_mut()
.unwrap()
.prepaint(global_id, inspector_id, bounds, window, cx);
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
_: &mut (),
window: &mut Window,
cx: &mut App
) {
self.cached_element
.as_mut()
.unwrap()
.paint(global_id, inspector_id, bounds, window, cx);
}
}
// Usage
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
MemoizedElement::new(
ElementId::Name("memoized".into()),
self.expensive_value.clone(),
|value| {
// Expensive rendering function only called when value changes
div().child(format!("Computed: {}", value))
}
)
}
Virtual List Pattern
Efficiently render large lists by only rendering visible items.
pub struct VirtualList {
id: ElementId,
item_count: usize,
item_height: Pixels,
viewport_height: Pixels,
scroll_offset: Pixels,
render_item: Box<dyn Fn(usize) -> AnyElement>,
}
struct VirtualListState {
visible_range: Range<usize>,
visible_item_layouts: Vec<LayoutId>,
}
impl Element for VirtualList {
type RequestLayoutState = VirtualListState;
type PrepaintState = Hitbox;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, VirtualListState) {
// Calculate visible range
let start_idx = (self.scroll_offset / self.item_height).floor() as usize;
let end_idx = ((self.scroll_offset + self.viewport_height) / self.item_height)
.ceil() as usize;
let visible_range = start_idx..end_idx.min(self.item_count);
// Request layout only for visible items
let visible_item_layouts: Vec<_> = visible_range.clone()
.map(|i| {
let mut item = (self.render_item)(i);
item.request_layout(global_id, inspector_id, window, cx).0
})
.collect();
let total_height = self.item_height * self.item_count as f32;
let layout_id = window.request_layout(
Style {
size: size(relative(1.0), self.viewport_height),
overflow: Overflow::Hidden,
..default()
},
visible_item_layouts.clone(),
cx
);
(layout_id, VirtualListState {
visible_range,
visible_item_layouts,
})
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
window: &mut Window,
_cx: &mut App
) -> Hitbox {
// Prepaint visible items at correct positions
for (i, layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
// Prepaint if visible
if item_bounds.intersects(&bounds) {
// Prepaint item...
}
}
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Paint visible items
for (i, _layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
if item_bounds.intersects(&bounds) {
let mut item = (self.render_item)(item_idx);
item.paint(item_bounds, window, cx);
}
}
// Handle scroll
window.on_mouse_event({
let hitbox = hitbox.clone();
let total_height = self.item_height * self.item_count as f32;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
self.scroll_offset -= event.delta.y;
self.scroll_offset = self.scroll_offset
.max(px(0.))
.min(total_height - self.viewport_height);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
// Usage: Efficiently render 10,000 items
let virtual_list = VirtualList {
id: ElementId::Name("large-list".into()),
item_count: 10_000,
item_height: px(40.),
viewport_height: px(400.),
scroll_offset: px(0.),
render_item: Box::new(|index| {
div().child(format!("Item {}", index))
}),
};
These advanced patterns enable sophisticated element implementations while maintaining performance and code quality.