633 lines
18 KiB
Markdown
633 lines
18 KiB
Markdown
# Element Implementation Examples
|
|
|
|
Complete examples of implementing custom elements for various scenarios.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Simple Text Element](#simple-text-element)
|
|
2. [Interactive Element with Selection](#interactive-element-with-selection)
|
|
3. [Complex Element with Child Management](#complex-element-with-child-management)
|
|
|
|
## Simple Text Element
|
|
|
|
A basic text element with syntax highlighting support.
|
|
|
|
```rust
|
|
pub struct SimpleText {
|
|
id: ElementId,
|
|
text: SharedString,
|
|
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
|
}
|
|
|
|
impl IntoElement for SimpleText {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Element for SimpleText {
|
|
type RequestLayoutState = StyledText;
|
|
type PrepaintState = Hitbox;
|
|
|
|
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, Self::RequestLayoutState) {
|
|
// Create styled text with highlights
|
|
let mut runs = Vec::new();
|
|
let mut ix = 0;
|
|
|
|
for (range, highlight) in &self.highlights {
|
|
// Add unstyled text before highlight
|
|
if ix < range.start {
|
|
runs.push(window.text_style().to_run(range.start - ix));
|
|
}
|
|
|
|
// Add highlighted text
|
|
runs.push(
|
|
window.text_style()
|
|
.highlight(*highlight)
|
|
.to_run(range.len())
|
|
);
|
|
ix = range.end;
|
|
}
|
|
|
|
// Add remaining unstyled text
|
|
if ix < self.text.len() {
|
|
runs.push(window.text_style().to_run(self.text.len() - ix));
|
|
}
|
|
|
|
let styled_text = StyledText::new(self.text.clone()).with_runs(runs);
|
|
let (layout_id, _) = styled_text.request_layout(
|
|
global_id,
|
|
inspector_id,
|
|
window,
|
|
cx
|
|
);
|
|
|
|
(layout_id, styled_text)
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
styled_text: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App
|
|
) -> Self::PrepaintState {
|
|
// Prepaint the styled text
|
|
styled_text.prepaint(
|
|
global_id,
|
|
inspector_id,
|
|
bounds,
|
|
&mut (),
|
|
window,
|
|
cx
|
|
);
|
|
|
|
// Create hitbox for interaction
|
|
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
|
hitbox
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
styled_text: &mut Self::RequestLayoutState,
|
|
hitbox: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App
|
|
) {
|
|
// Paint the styled text
|
|
styled_text.paint(
|
|
global_id,
|
|
inspector_id,
|
|
bounds,
|
|
&mut (),
|
|
&mut (),
|
|
window,
|
|
cx
|
|
);
|
|
|
|
// Set cursor style for text
|
|
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Interactive Element with Selection
|
|
|
|
A text element that supports text selection via mouse interaction.
|
|
|
|
```rust
|
|
#[derive(Clone)]
|
|
pub struct Selection {
|
|
pub start: usize,
|
|
pub end: usize,
|
|
}
|
|
|
|
pub struct SelectableText {
|
|
id: ElementId,
|
|
text: SharedString,
|
|
selectable: bool,
|
|
selection: Option<Selection>,
|
|
}
|
|
|
|
impl IntoElement for SelectableText {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Element for SelectableText {
|
|
type RequestLayoutState = TextLayout;
|
|
type PrepaintState = Option<Hitbox>;
|
|
|
|
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, Self::RequestLayoutState) {
|
|
let styled_text = StyledText::new(self.text.clone());
|
|
let (layout_id, _) = styled_text.request_layout(
|
|
global_id,
|
|
inspector_id,
|
|
window,
|
|
cx
|
|
);
|
|
|
|
// Extract text layout for selection painting
|
|
let text_layout = styled_text.layout().clone();
|
|
|
|
(layout_id, text_layout)
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
_global_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
_text_layout: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
_cx: &mut App
|
|
) -> Self::PrepaintState {
|
|
// Only create hitbox if selectable
|
|
if self.selectable {
|
|
Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
text_layout: &mut Self::RequestLayoutState,
|
|
hitbox: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App
|
|
) {
|
|
// Paint text
|
|
let styled_text = StyledText::new(self.text.clone());
|
|
styled_text.paint(
|
|
global_id,
|
|
inspector_id,
|
|
bounds,
|
|
&mut (),
|
|
&mut (),
|
|
window,
|
|
cx
|
|
);
|
|
|
|
// Paint selection if any
|
|
if let Some(selection) = &self.selection {
|
|
Self::paint_selection(selection, text_layout, &bounds, window, cx);
|
|
}
|
|
|
|
// Handle mouse events for selection
|
|
if let Some(hitbox) = hitbox {
|
|
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
|
|
|
// Mouse down to start selection
|
|
window.on_mouse_event({
|
|
let bounds = bounds.clone();
|
|
move |event: &MouseDownEvent, phase, window, cx| {
|
|
if bounds.contains(&event.position) && phase.bubble() {
|
|
// Start selection at mouse position
|
|
let char_index = Self::position_to_index(
|
|
event.position,
|
|
&bounds,
|
|
text_layout
|
|
);
|
|
self.selection = Some(Selection {
|
|
start: char_index,
|
|
end: char_index,
|
|
});
|
|
cx.notify();
|
|
cx.stop_propagation();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Mouse drag to extend selection
|
|
window.on_mouse_event({
|
|
let bounds = bounds.clone();
|
|
move |event: &MouseMoveEvent, phase, window, cx| {
|
|
if let Some(selection) = &mut self.selection {
|
|
if phase.bubble() {
|
|
let char_index = Self::position_to_index(
|
|
event.position,
|
|
&bounds,
|
|
text_layout
|
|
);
|
|
selection.end = char_index;
|
|
cx.notify();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SelectableText {
|
|
fn paint_selection(
|
|
selection: &Selection,
|
|
text_layout: &TextLayout,
|
|
bounds: &Bounds<Pixels>,
|
|
window: &mut Window,
|
|
cx: &mut App
|
|
) {
|
|
// Calculate selection bounds from text layout
|
|
let selection_rects = text_layout.rects_for_range(
|
|
selection.start..selection.end
|
|
);
|
|
|
|
// Paint selection background
|
|
for rect in selection_rects {
|
|
window.paint_quad(paint_quad(
|
|
Bounds::new(
|
|
point(bounds.left() + rect.origin.x, bounds.top() + rect.origin.y),
|
|
rect.size
|
|
),
|
|
Corners::default(),
|
|
cx.theme().selection_background,
|
|
));
|
|
}
|
|
}
|
|
|
|
fn position_to_index(
|
|
position: Point<Pixels>,
|
|
bounds: &Bounds<Pixels>,
|
|
text_layout: &TextLayout
|
|
) -> usize {
|
|
// Convert screen position to character index
|
|
let relative_pos = point(
|
|
position.x - bounds.left(),
|
|
position.y - bounds.top()
|
|
);
|
|
text_layout.index_for_position(relative_pos)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Complex Element with Child Management
|
|
|
|
A container element that manages multiple children with scrolling support.
|
|
|
|
```rust
|
|
pub struct ComplexElement {
|
|
id: ElementId,
|
|
children: Vec<Box<dyn Element<RequestLayoutState = (), PrepaintState = ()>>>,
|
|
scrollable: bool,
|
|
scroll_offset: Point<Pixels>,
|
|
}
|
|
|
|
struct ComplexLayoutState {
|
|
child_layouts: Vec<LayoutId>,
|
|
total_height: Pixels,
|
|
}
|
|
|
|
struct ComplexPaintState {
|
|
child_bounds: Vec<Bounds<Pixels>>,
|
|
hitbox: Hitbox,
|
|
}
|
|
|
|
impl IntoElement for ComplexElement {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Element for ComplexElement {
|
|
type RequestLayoutState = ComplexLayoutState;
|
|
type PrepaintState = ComplexPaintState;
|
|
|
|
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, Self::RequestLayoutState) {
|
|
let mut child_layouts = Vec::new();
|
|
let mut total_height = px(0.);
|
|
|
|
// Request layout for all children
|
|
for child in &mut self.children {
|
|
let (child_layout_id, _) = child.request_layout(
|
|
global_id,
|
|
inspector_id,
|
|
window,
|
|
cx
|
|
);
|
|
child_layouts.push(child_layout_id);
|
|
|
|
// Get child size from layout
|
|
let child_size = window.layout_bounds(child_layout_id).size();
|
|
total_height += child_size.height;
|
|
}
|
|
|
|
// Create container layout
|
|
let layout_id = window.request_layout(
|
|
Style {
|
|
flex_direction: FlexDirection::Column,
|
|
gap: px(8.),
|
|
size: Size {
|
|
width: relative(1.0),
|
|
height: if self.scrollable {
|
|
// Fixed height for scrollable
|
|
px(400.)
|
|
} else {
|
|
// Auto height for non-scrollable
|
|
total_height
|
|
},
|
|
},
|
|
..default()
|
|
},
|
|
child_layouts.clone(),
|
|
cx
|
|
);
|
|
|
|
(layout_id, ComplexLayoutState {
|
|
child_layouts,
|
|
total_height,
|
|
})
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
layout_state: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App
|
|
) -> Self::PrepaintState {
|
|
let mut child_bounds = Vec::new();
|
|
let mut y_offset = self.scroll_offset.y;
|
|
|
|
// Calculate child bounds and prepaint children
|
|
for (child, layout_id) in self.children.iter_mut()
|
|
.zip(&layout_state.child_layouts)
|
|
{
|
|
let child_size = window.layout_bounds(*layout_id).size();
|
|
let child_bound = Bounds::new(
|
|
point(bounds.left(), bounds.top() + y_offset),
|
|
child_size
|
|
);
|
|
|
|
// Only prepaint visible children
|
|
if self.is_visible(&child_bound, &bounds) {
|
|
child.prepaint(
|
|
global_id,
|
|
inspector_id,
|
|
child_bound,
|
|
&mut (),
|
|
window,
|
|
cx
|
|
);
|
|
}
|
|
|
|
child_bounds.push(child_bound);
|
|
y_offset += child_size.height + px(8.); // gap
|
|
}
|
|
|
|
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
|
|
|
ComplexPaintState {
|
|
child_bounds,
|
|
hitbox,
|
|
}
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
layout_state: &mut Self::RequestLayoutState,
|
|
paint_state: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App
|
|
) {
|
|
// Paint background
|
|
window.paint_quad(paint_quad(
|
|
bounds,
|
|
Corners::all(px(4.)),
|
|
cx.theme().background,
|
|
));
|
|
|
|
// Paint visible children only
|
|
for (i, child) in self.children.iter_mut().enumerate() {
|
|
let child_bounds = paint_state.child_bounds[i];
|
|
|
|
if self.is_visible(&child_bounds, &bounds) {
|
|
child.paint(
|
|
global_id,
|
|
inspector_id,
|
|
child_bounds,
|
|
&mut (),
|
|
&mut (),
|
|
window,
|
|
cx
|
|
);
|
|
}
|
|
}
|
|
|
|
// Paint scrollbar if scrollable
|
|
if self.scrollable {
|
|
self.paint_scrollbar(bounds, layout_state, window, cx);
|
|
}
|
|
|
|
// Handle scroll events
|
|
if self.scrollable {
|
|
window.on_mouse_event({
|
|
let hitbox = paint_state.hitbox.clone();
|
|
let total_height = layout_state.total_height;
|
|
let visible_height = 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 scroll offset
|
|
let max_scroll = (total_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 ComplexElement {
|
|
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
|
|
// Check if child is within visible area
|
|
child_bounds.bottom() >= container_bounds.top() &&
|
|
child_bounds.top() <= container_bounds.bottom()
|
|
}
|
|
|
|
fn paint_scrollbar(
|
|
&self,
|
|
bounds: Bounds<Pixels>,
|
|
layout_state: &ComplexLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App
|
|
) {
|
|
let scrollbar_width = px(8.);
|
|
let visible_height = bounds.size.height;
|
|
let total_height = layout_state.total_height;
|
|
|
|
if total_height <= visible_height {
|
|
return; // No need for scrollbar
|
|
}
|
|
|
|
// Calculate scrollbar position and size
|
|
let scroll_ratio = self.scroll_offset.y / (total_height - visible_height);
|
|
let thumb_height = (visible_height / total_height) * visible_height;
|
|
let thumb_y = scroll_ratio * (visible_height - thumb_height);
|
|
|
|
// Paint scrollbar track
|
|
let track_bounds = Bounds::new(
|
|
point(bounds.right() - scrollbar_width, bounds.top()),
|
|
size(scrollbar_width, visible_height)
|
|
);
|
|
window.paint_quad(paint_quad(
|
|
track_bounds,
|
|
Corners::default(),
|
|
cx.theme().scrollbar_track,
|
|
));
|
|
|
|
// Paint scrollbar thumb
|
|
let thumb_bounds = Bounds::new(
|
|
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
|
|
size(scrollbar_width, thumb_height)
|
|
);
|
|
window.paint_quad(paint_quad(
|
|
thumb_bounds,
|
|
Corners::all(px(4.)),
|
|
cx.theme().scrollbar_thumb,
|
|
));
|
|
}
|
|
}
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Using SimpleText
|
|
|
|
```rust
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
.child(SimpleText {
|
|
id: ElementId::Name("code-text".into()),
|
|
text: "fn main() { println!(\"Hello\"); }".into(),
|
|
highlights: vec![
|
|
(0..2, HighlightStyle::keyword()),
|
|
(3..7, HighlightStyle::function()),
|
|
],
|
|
})
|
|
}
|
|
```
|
|
|
|
### Using SelectableText
|
|
|
|
```rust
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
.child(SelectableText {
|
|
id: ElementId::Name("selectable-text".into()),
|
|
text: "Select this text with your mouse".into(),
|
|
selectable: true,
|
|
selection: self.current_selection.clone(),
|
|
})
|
|
}
|
|
```
|
|
|
|
### Using ComplexElement
|
|
|
|
```rust
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let children: Vec<Box<dyn Element<_, _>>> = self.items
|
|
.iter()
|
|
.map(|item| Box::new(div().child(item.name.clone())) as Box<_>)
|
|
.collect();
|
|
|
|
div()
|
|
.child(ComplexElement {
|
|
id: ElementId::Name("scrollable-list".into()),
|
|
children,
|
|
scrollable: true,
|
|
scroll_offset: self.scroll_offset,
|
|
})
|
|
}
|
|
```
|