Reviewed-on: #18 Co-authored-by: Ren Amamiya <reya@lume.nu> Co-committed-by: Ren Amamiya <reya@lume.nu>
18 KiB
18 KiB
Element Implementation Examples
Complete examples of implementing custom elements for various scenarios.
Table of Contents
Simple Text Element
A basic text element with syntax highlighting support.
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.
#[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.
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
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
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
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,
})
}