Reviewed-on: #18 Co-authored-by: Ren Amamiya <reya@lume.nu> Co-committed-by: Ren Amamiya <reya@lume.nu>
14 KiB
14 KiB
Common Element Patterns
Reusable patterns for implementing common element types in GPUI.
Text Rendering Elements
Elements that display and manipulate text content.
Pattern Characteristics
- Use
StyledTextfor text layout and rendering - Handle text selection in
paintphase with hitbox interaction - Create hitboxes for text interaction in
prepaint - Support text highlighting and custom styling via runs
Implementation Template
pub struct TextElement {
id: ElementId,
text: SharedString,
style: TextStyle,
}
impl Element for TextElement {
type RequestLayoutState = StyledText;
type PrepaintState = Hitbox;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, StyledText)
{
let styled_text = StyledText::new(self.text.clone())
.with_style(self.style);
let (layout_id, _) = styled_text.request_layout(None, None, window, cx);
(layout_id, styled_text)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
window: &mut Window, cx: &mut App) -> Hitbox
{
styled_text.prepaint(None, None, bounds, &mut (), window, cx);
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
hitbox: &mut Hitbox, window: &mut Window, cx: &mut App)
{
styled_text.paint(None, None, bounds, &mut (), &mut (), window, cx);
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
}
Use Cases
- Code editors with syntax highlighting
- Rich text displays
- Labels with custom formatting
- Selectable text areas
Container Elements
Elements that manage and layout child elements.
Pattern Characteristics
- Manage child element layouts and positions
- Handle scrolling and clipping when needed
- Implement flex/grid-like layouts
- Coordinate child interactions and event delegation
Implementation Template
pub struct ContainerElement {
id: ElementId,
children: Vec<AnyElement>,
direction: FlexDirection,
gap: Pixels,
}
impl Element for ContainerElement {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<LayoutId>)
{
let child_layout_ids: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(window, cx).0)
.collect();
let layout_id = window.request_layout(
Style {
flex_direction: self.direction,
gap: self.gap,
..default()
},
child_layout_ids.clone(),
cx
);
(layout_id, child_layout_ids)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout_ids: &mut Vec<LayoutId>,
window: &mut Window, cx: &mut App) -> Vec<Bounds<Pixels>>
{
let mut child_bounds = Vec::new();
for (child, layout_id) in self.children.iter_mut().zip(layout_ids.iter()) {
let child_bound = window.layout_bounds(*layout_id);
child.prepaint(child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(&mut self, .., child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window, cx: &mut App)
{
for (child, bounds) in self.children.iter_mut().zip(child_bounds.iter()) {
child.paint(*bounds, window, cx);
}
}
}
Use Cases
- Panels and split views
- List containers
- Grid layouts
- Tab containers
Interactive Elements
Elements that respond to user input (mouse, keyboard, touch).
Pattern Characteristics
- Create appropriate hitboxes for interaction areas
- Handle mouse/keyboard/touch events properly
- Manage focus and cursor styles
- Support hover, active, and disabled states
Implementation Template
pub struct InteractiveElement {
id: ElementId,
content: AnyElement,
on_click: Option<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
hover_style: Option<Style>,
}
impl Element for InteractiveElement {
type RequestLayoutState = LayoutId;
type PrepaintState = (Hitbox, bool); // hitbox and is_hovered
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, LayoutId)
{
let (content_layout, _) = self.content.request_layout(window, cx);
(content_layout, content_layout)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
window: &mut Window, cx: &mut App) -> (Hitbox, bool)
{
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
let is_hovered = hitbox.is_hovered(window);
self.content.prepaint(bounds, window, cx);
(hitbox, is_hovered)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
prepaint: &mut (Hitbox, bool), window: &mut Window, cx: &mut App)
{
let (hitbox, is_hovered) = prepaint;
// Paint hover background if hovered
if *is_hovered {
if let Some(hover_style) = &self.hover_style {
window.paint_quad(paint_quad(
bounds,
Corners::all(px(4.)),
hover_style.background_color.unwrap_or(cx.theme().hover),
));
}
}
// Paint content
self.content.paint(bounds, window, cx);
// Handle click
if let Some(on_click) = self.on_click.as_ref() {
window.on_mouse_event({
let on_click = on_click.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
on_click(event, window, cx);
cx.stop_propagation();
}
}
});
}
// Set cursor style
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}
Use Cases
- Buttons
- Links
- Clickable cards
- Drag handles
- Menu items
Composite Elements
Elements that combine multiple child elements with complex coordination.
Pattern Characteristics
- Combine multiple child elements with different types
- Manage complex state across children
- Coordinate animations and transitions
- Handle focus delegation between children
Implementation Template
pub struct CompositeElement {
id: ElementId,
header: AnyElement,
content: AnyElement,
footer: Option<AnyElement>,
}
struct CompositeLayoutState {
header_layout: LayoutId,
content_layout: LayoutId,
footer_layout: Option<LayoutId>,
}
struct CompositePaintState {
header_bounds: Bounds<Pixels>,
content_bounds: Bounds<Pixels>,
footer_bounds: Option<Bounds<Pixels>>,
}
impl Element for CompositeElement {
type RequestLayoutState = CompositeLayoutState;
type PrepaintState = CompositePaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, CompositeLayoutState)
{
let (header_layout, _) = self.header.request_layout(window, cx);
let (content_layout, _) = self.content.request_layout(window, cx);
let footer_layout = self.footer.as_mut()
.map(|f| f.request_layout(window, cx).0);
let mut children = vec![header_layout, content_layout];
if let Some(footer) = footer_layout {
children.push(footer);
}
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Column,
size: Size {
width: relative(1.0),
height: auto(),
},
..default()
},
children,
cx
);
(layout_id, CompositeLayoutState {
header_layout,
content_layout,
footer_layout,
})
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut CompositeLayoutState,
window: &mut Window, cx: &mut App) -> CompositePaintState
{
let header_bounds = window.layout_bounds(layout.header_layout);
let content_bounds = window.layout_bounds(layout.content_layout);
let footer_bounds = layout.footer_layout
.map(|id| window.layout_bounds(id));
self.header.prepaint(header_bounds, window, cx);
self.content.prepaint(content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, footer_bounds) {
footer.prepaint(bounds, window, cx);
}
CompositePaintState {
header_bounds,
content_bounds,
footer_bounds,
}
}
fn paint(&mut self, .., paint_state: &mut CompositePaintState,
window: &mut Window, cx: &mut App)
{
self.header.paint(paint_state.header_bounds, window, cx);
self.content.paint(paint_state.content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, paint_state.footer_bounds) {
footer.paint(bounds, window, cx);
}
}
}
Use Cases
- Dialog boxes (header + content + footer)
- Cards with multiple sections
- Form layouts
- Panels with toolbars
Scrollable Elements
Elements with scrollable content areas.
Pattern Characteristics
- Manage scroll state (offset, velocity)
- Handle scroll events (wheel, drag, touch)
- Paint scrollbars (track and thumb)
- Clip content to visible area
Implementation Template
pub struct ScrollableElement {
id: ElementId,
content: AnyElement,
scroll_offset: Point<Pixels>,
content_size: Size<Pixels>,
}
struct ScrollPaintState {
hitbox: Hitbox,
visible_bounds: Bounds<Pixels>,
}
impl Element for ScrollableElement {
type RequestLayoutState = (LayoutId, Size<Pixels>);
type PrepaintState = ScrollPaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, (LayoutId, Size<Pixels>))
{
let (content_layout, _) = self.content.request_layout(window, cx);
let content_size = window.layout_bounds(content_layout).size;
let layout_id = window.request_layout(
Style {
size: Size {
width: relative(1.0),
height: px(400.), // Fixed viewport height
},
overflow: Overflow::Hidden,
..default()
},
vec![content_layout],
cx
);
(layout_id, (content_layout, content_size))
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut (LayoutId, Size<Pixels>),
window: &mut Window, cx: &mut App) -> ScrollPaintState
{
let (content_layout, content_size) = layout;
// Calculate content bounds with scroll offset
let content_bounds = Bounds::new(
point(bounds.left(), bounds.top() - self.scroll_offset.y),
*content_size
);
self.content.prepaint(content_bounds, window, cx);
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
ScrollPaintState {
hitbox,
visible_bounds: bounds,
}
}
fn paint(&mut self, .., layout: &mut (LayoutId, Size<Pixels>),
paint_state: &mut ScrollPaintState, window: &mut Window, cx: &mut App)
{
let (_, content_size) = layout;
// Paint content
self.content.paint(paint_state.visible_bounds, window, cx);
// Paint scrollbar
self.paint_scrollbar(paint_state.visible_bounds, *content_size, window, cx);
// Handle scroll events
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
let content_height = content_size.height;
let visible_height = paint_state.visible_bounds.size.height;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Update scroll offset
self.scroll_offset.y -= event.delta.y;
// Clamp to valid range
let max_scroll = (content_height - visible_height).max(px(0.));
self.scroll_offset.y = self.scroll_offset.y
.max(px(0.))
.min(max_scroll);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
impl ScrollableElement {
fn paint_scrollbar(
&self,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
window: &mut Window,
cx: &mut App
) {
let visible_height = bounds.size.height;
let content_height = content_size.height;
if content_height <= visible_height {
return; // No scrollbar needed
}
let scrollbar_width = px(8.);
// Calculate thumb position and size
let scroll_ratio = self.scroll_offset.y / (content_height - visible_height);
let thumb_height = (visible_height / content_height) * visible_height;
let thumb_y = scroll_ratio * (visible_height - thumb_height);
// Paint track
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top()),
size(scrollbar_width, visible_height)
),
Corners::default(),
cx.theme().scrollbar_track,
));
// Paint thumb
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
size(scrollbar_width, thumb_height)
),
Corners::all(px(4.)),
cx.theme().scrollbar_thumb,
));
}
}
Use Cases
- Scrollable lists
- Code editors with large files
- Long-form text content
- Image galleries
Pattern Selection Guide
| Need | Pattern | Complexity |
|---|---|---|
| Display styled text | Text Rendering | Low |
| Layout multiple children | Container | Low-Medium |
| Handle clicks/hovers | Interactive | Medium |
| Complex multi-part UI | Composite | Medium-High |
| Large content with scrolling | Scrollable | High |
Choose the simplest pattern that meets your requirements, then extend as needed.