Style primitives
This commit is contained in:
@@ -4,12 +4,12 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use ruin_runtime::{TimeoutHandle, clear_timeout, set_timeout};
|
use ruin_runtime::{TimeoutHandle, clear_timeout, set_timeout};
|
||||||
use ruin_ui::{
|
use ruin_ui::{
|
||||||
Color, CursorIcon, DisplayItem, Edges, Element, ElementId, ImageFit, ImageResource,
|
BoxShadow, BoxShadowKind, Color, CursorIcon, DisplayItem, Edges, Element, ElementId, ImageFit,
|
||||||
InteractionTree, KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutSnapshot, PlatformEvent,
|
ImageResource, InteractionTree, KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutSnapshot,
|
||||||
PointerButton, PointerEvent, PointerEventKind, PointerRouter, PreparedText, Quad,
|
PlatformEvent, PointerButton, PointerEvent, PointerEventKind, PointerRouter, PreparedText,
|
||||||
RoutedPointerEventKind, SceneSnapshot, TextAlign, TextFontFamily, TextSelectionStyle, TextSpan,
|
Quad, RoutedPointerEventKind, SceneSnapshot, TextAlign, TextFontFamily, TextSelectionStyle,
|
||||||
TextSpanSlant, TextSpanWeight, TextStyle, TextSystem, TextWrap, UiSize, WindowController,
|
TextSpan, TextSpanSlant, TextSpanWeight, TextStyle, TextSystem, TextWrap, UiSize,
|
||||||
WindowSpec, WindowUpdate, layout_snapshot_with_text_system,
|
WindowController, WindowSpec, WindowUpdate, layout_snapshot_with_text_system,
|
||||||
};
|
};
|
||||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
@@ -1475,6 +1475,10 @@ fn build_document_tree(
|
|||||||
.padding(Edges::all(gutter))
|
.padding(Edges::all(gutter))
|
||||||
.gap(gutter * 0.45)
|
.gap(gutter * 0.45)
|
||||||
.background(card_background(NEXT_CARD_ID, hovered_card))
|
.background(card_background(NEXT_CARD_ID, hovered_card))
|
||||||
|
.border(2.0, card_border_color(NEXT_CARD_ID, hovered_card))
|
||||||
|
.corner_radius(gutter * 0.6)
|
||||||
|
.shadow(card_key_shadow(NEXT_CARD_ID, hovered_card))
|
||||||
|
.shadow(card_ambient_shadow(NEXT_CARD_ID, hovered_card))
|
||||||
.children([
|
.children([
|
||||||
Element::paragraph(
|
Element::paragraph(
|
||||||
"Next direction",
|
"Next direction",
|
||||||
@@ -1669,3 +1673,44 @@ fn card_title_color(id: ElementId, hovered_card: Option<ElementId>) -> Color {
|
|||||||
}
|
}
|
||||||
Color::rgb(0xF4, 0xF7, 0xFF)
|
Color::rgb(0xF4, 0xF7, 0xFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn card_border_color(id: ElementId, hovered_card: Option<ElementId>) -> Color {
|
||||||
|
if hovered_card == Some(id) {
|
||||||
|
return Color::rgb(0xF5, 0xD0, 0x74);
|
||||||
|
}
|
||||||
|
|
||||||
|
match id {
|
||||||
|
NEXT_CARD_ID => Color::rgba(0x8B, 0x99, 0xAD, 0x78),
|
||||||
|
_ => Color::rgba(0x00, 0x00, 0x00, 0x00),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card_key_shadow(id: ElementId, hovered_card: Option<ElementId>) -> BoxShadow {
|
||||||
|
let hovered = hovered_card == Some(id);
|
||||||
|
BoxShadow::new(
|
||||||
|
ruin_ui::Point::new(0.0, if hovered { 16.0 } else { 12.0 }),
|
||||||
|
if hovered { 28.0 } else { 22.0 },
|
||||||
|
if hovered { -4.0 } else { -6.0 },
|
||||||
|
if hovered {
|
||||||
|
Color::rgba(0x04, 0x07, 0x0D, 0xC4)
|
||||||
|
} else {
|
||||||
|
Color::rgba(0x04, 0x07, 0x0D, 0x96)
|
||||||
|
},
|
||||||
|
BoxShadowKind::Outer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card_ambient_shadow(id: ElementId, hovered_card: Option<ElementId>) -> BoxShadow {
|
||||||
|
let hovered = hovered_card == Some(id);
|
||||||
|
BoxShadow::new(
|
||||||
|
ruin_ui::Point::new(0.0, if hovered { 4.0 } else { 2.0 }),
|
||||||
|
if hovered { 10.0 } else { 8.0 },
|
||||||
|
0.0,
|
||||||
|
if hovered {
|
||||||
|
Color::rgba(0x0C, 0x16, 0x27, 0x88)
|
||||||
|
} else {
|
||||||
|
Color::rgba(0x0C, 0x16, 0x27, 0x62)
|
||||||
|
},
|
||||||
|
BoxShadowKind::Outer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ mod tests {
|
|||||||
path: LayoutPath::root(),
|
path: LayoutPath::root(),
|
||||||
element_id: None,
|
element_id: None,
|
||||||
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
||||||
|
corner_radius: 0.0,
|
||||||
pointer_events: false,
|
pointer_events: false,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
@@ -174,6 +175,7 @@ mod tests {
|
|||||||
path: LayoutPath::root().child(0),
|
path: LayoutPath::root().child(0),
|
||||||
element_id: Some(ElementId::new(1)),
|
element_id: Some(ElementId::new(1)),
|
||||||
rect: Rect::new(0.0, 0.0, 120.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 120.0, 120.0),
|
||||||
|
corner_radius: 0.0,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
@@ -185,6 +187,7 @@ mod tests {
|
|||||||
path: LayoutPath::root().child(1),
|
path: LayoutPath::root().child(1),
|
||||||
element_id: Some(ElementId::new(2)),
|
element_id: Some(ElementId::new(2)),
|
||||||
rect: Rect::new(80.0, 0.0, 120.0, 120.0),
|
rect: Rect::new(80.0, 0.0, 120.0, 120.0),
|
||||||
|
corner_radius: 0.0,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
@@ -203,6 +206,7 @@ mod tests {
|
|||||||
path: LayoutPath::root(),
|
path: LayoutPath::root(),
|
||||||
element_id: None,
|
element_id: None,
|
||||||
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 200.0, 120.0),
|
||||||
|
corner_radius: 0.0,
|
||||||
pointer_events: false,
|
pointer_events: false,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
@@ -212,6 +216,7 @@ mod tests {
|
|||||||
path: LayoutPath::root().child(0),
|
path: LayoutPath::root().child(0),
|
||||||
element_id: Some(ElementId::new(1)),
|
element_id: Some(ElementId::new(1)),
|
||||||
rect: Rect::new(0.0, 0.0, 160.0, 120.0),
|
rect: Rect::new(0.0, 0.0, 160.0, 120.0),
|
||||||
|
corner_radius: 0.0,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
@@ -221,6 +226,7 @@ mod tests {
|
|||||||
path: LayoutPath::root().child(0).child(0),
|
path: LayoutPath::root().child(0).child(0),
|
||||||
element_id: Some(ElementId::new(2)),
|
element_id: Some(ElementId::new(2)),
|
||||||
rect: Rect::new(16.0, 16.0, 80.0, 40.0),
|
rect: Rect::new(16.0, 16.0, 80.0, 40.0),
|
||||||
|
corner_radius: 0.0,
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: CursorIcon::Default,
|
cursor: CursorIcon::Default,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::ImageFit;
|
use crate::ImageFit;
|
||||||
use crate::scene::{PreparedImage, PreparedText, Rect, SceneSnapshot, UiSize};
|
use crate::scene::{
|
||||||
|
PreparedImage, PreparedText, Rect, RoundedRect, SceneSnapshot, ShadowRect, UiSize,
|
||||||
|
};
|
||||||
use crate::text::TextSystem;
|
use crate::text::TextSystem;
|
||||||
use crate::tree::{CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode};
|
use crate::tree::{CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style};
|
||||||
|
|
||||||
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
||||||
let mut text_system = TextSystem::new();
|
let mut text_system = TextSystem::new();
|
||||||
@@ -58,6 +60,7 @@ pub struct LayoutNode {
|
|||||||
pub path: LayoutPath,
|
pub path: LayoutPath,
|
||||||
pub element_id: Option<ElementId>,
|
pub element_id: Option<ElementId>,
|
||||||
pub rect: Rect,
|
pub rect: Rect,
|
||||||
|
pub corner_radius: f32,
|
||||||
pub pointer_events: bool,
|
pub pointer_events: bool,
|
||||||
pub focusable: bool,
|
pub focusable: bool,
|
||||||
pub cursor: CursorIcon,
|
pub cursor: CursorIcon,
|
||||||
@@ -187,6 +190,7 @@ fn layout_element(
|
|||||||
path,
|
path,
|
||||||
element_id: element.id,
|
element_id: element.id,
|
||||||
rect,
|
rect,
|
||||||
|
corner_radius: uniform_corner_radius(&element.style, rect),
|
||||||
pointer_events: element.style.pointer_events,
|
pointer_events: element.style.pointer_events,
|
||||||
focusable: element.style.focusable,
|
focusable: element.style.focusable,
|
||||||
cursor,
|
cursor,
|
||||||
@@ -199,14 +203,32 @@ fn layout_element(
|
|||||||
return interaction;
|
return interaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(color) = element.style.background {
|
push_box_shadows(
|
||||||
perf_stats.background_quads += 1;
|
scene,
|
||||||
scene.push_quad(rect, color);
|
rect,
|
||||||
}
|
&element.style,
|
||||||
|
crate::BoxShadowKind::Outer,
|
||||||
|
perf_stats,
|
||||||
|
);
|
||||||
|
push_container_fill_and_border(scene, rect, &element.style, perf_stats);
|
||||||
|
push_box_shadows(
|
||||||
|
scene,
|
||||||
|
rect,
|
||||||
|
&element.style,
|
||||||
|
crate::BoxShadowKind::Inner,
|
||||||
|
perf_stats,
|
||||||
|
);
|
||||||
|
let clip_radius = uniform_corner_radius(&element.style, rect);
|
||||||
|
let pushed_clip = if clip_radius > 0.0 {
|
||||||
|
scene.push_clip(rect, clip_radius);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(text) = element.text_node() {
|
if let Some(text) = element.text_node() {
|
||||||
perf_stats.text_nodes += 1;
|
perf_stats.text_nodes += 1;
|
||||||
let content = inset_rect(rect, element.style.padding);
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
if content.size.width > 0.0 && content.size.height > 0.0 {
|
if content.size.width > 0.0 && content.size.height > 0.0 {
|
||||||
perf_stats.text_prepare_calls += 1;
|
perf_stats.text_prepare_calls += 1;
|
||||||
let prepare_started = perf_stats.enabled.then(Instant::now);
|
let prepare_started = perf_stats.enabled.then(Instant::now);
|
||||||
@@ -223,16 +245,22 @@ fn layout_element(
|
|||||||
perf_stats.text_prepare_ms += prepare_started.elapsed().as_secs_f64() * 1_000.0;
|
perf_stats.text_prepare_ms += prepare_started.elapsed().as_secs_f64() * 1_000.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if pushed_clip {
|
||||||
|
scene.pop_clip();
|
||||||
|
}
|
||||||
return interaction;
|
return interaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(image) = element.image_node() {
|
if let Some(image) = element.image_node() {
|
||||||
let content = inset_rect(rect, element.style.padding);
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
if content.size.width > 0.0 && content.size.height > 0.0 {
|
if content.size.width > 0.0 && content.size.height > 0.0 {
|
||||||
let prepared = prepare_image(image, content, element.id);
|
let prepared = prepare_image(image, content, element.id);
|
||||||
scene.push_image(prepared.clone());
|
scene.push_image(prepared.clone());
|
||||||
interaction.prepared_image = Some(prepared);
|
interaction.prepared_image = Some(prepared);
|
||||||
}
|
}
|
||||||
|
if pushed_clip {
|
||||||
|
scene.pop_clip();
|
||||||
|
}
|
||||||
return interaction;
|
return interaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +270,7 @@ fn layout_element(
|
|||||||
return interaction;
|
return interaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = inset_rect(rect, element.style.padding);
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||||
return interaction;
|
return interaction;
|
||||||
}
|
}
|
||||||
@@ -331,11 +359,15 @@ fn layout_element(
|
|||||||
cursor += child_main.max(0.0) + element.style.gap;
|
cursor += child_main.max(0.0) + element.style.gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pushed_clip {
|
||||||
|
scene.pop_clip();
|
||||||
|
}
|
||||||
|
|
||||||
interaction
|
interaction
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<HitTarget>> {
|
fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<HitTarget>> {
|
||||||
if !node.rect.contains(point) {
|
if !point_hits_node_shape(node, point) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +400,7 @@ fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<Hi
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn text_hit_test_node(node: &LayoutNode, point: crate::scene::Point) -> Option<TextHitTarget> {
|
fn text_hit_test_node(node: &LayoutNode, point: crate::scene::Point) -> Option<TextHitTarget> {
|
||||||
if !node.rect.contains(point) {
|
if !point_hits_node_shape(node, point) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +446,49 @@ fn text_for_element_node(node: &LayoutNode, element_id: ElementId) -> Option<&Pr
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn point_hits_node_shape(node: &LayoutNode, point: crate::scene::Point) -> bool {
|
||||||
|
node.rect.contains(point)
|
||||||
|
&& (node.corner_radius <= 0.0
|
||||||
|
|| point_in_rounded_rect(point, node.rect, node.corner_radius))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_in_rounded_rect(point: crate::scene::Point, rect: Rect, radius: f32) -> bool {
|
||||||
|
let radius = radius
|
||||||
|
.max(0.0)
|
||||||
|
.min(rect.size.width * 0.5)
|
||||||
|
.min(rect.size.height * 0.5);
|
||||||
|
if radius <= 0.0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = rect.origin.x;
|
||||||
|
let top = rect.origin.y;
|
||||||
|
let right = rect.origin.x + rect.size.width;
|
||||||
|
let bottom = rect.origin.y + rect.size.height;
|
||||||
|
|
||||||
|
if point.x >= left + radius && point.x < right - radius
|
||||||
|
|| point.y >= top + radius && point.y < bottom - radius
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let corner_center = if point.x < left + radius {
|
||||||
|
if point.y < top + radius {
|
||||||
|
crate::scene::Point::new(left + radius, top + radius)
|
||||||
|
} else {
|
||||||
|
crate::scene::Point::new(left + radius, bottom - radius)
|
||||||
|
}
|
||||||
|
} else if point.y < top + radius {
|
||||||
|
crate::scene::Point::new(right - radius, top + radius)
|
||||||
|
} else {
|
||||||
|
crate::scene::Point::new(right - radius, bottom - radius)
|
||||||
|
};
|
||||||
|
|
||||||
|
let dx = point.x - corner_center.x;
|
||||||
|
let dy = point.y - corner_center.y;
|
||||||
|
dx * dx + dy * dy <= radius * radius
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
struct MeasuredChild {
|
struct MeasuredChild {
|
||||||
cross: f32,
|
cross: f32,
|
||||||
@@ -436,7 +511,7 @@ fn intrinsic_main_size(
|
|||||||
};
|
};
|
||||||
let content =
|
let content =
|
||||||
text_system.measure_spans(&text.spans, &text.style, constraints.0, constraints.1);
|
text_system.measure_spans(&text.spans, &text.style, constraints.0, constraints.1);
|
||||||
let padding = main_axis_padding(child.style.padding, direction);
|
let padding = main_axis_padding(content_insets(&child.style), direction);
|
||||||
return main_axis_size(content, direction) + padding;
|
return main_axis_size(content, direction) + padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +537,7 @@ fn intrinsic_size(
|
|||||||
perf_stats: &mut LayoutPerfStats,
|
perf_stats: &mut LayoutPerfStats,
|
||||||
) -> UiSize {
|
) -> UiSize {
|
||||||
perf_stats.intrinsic_size_calls += 1;
|
perf_stats.intrinsic_size_calls += 1;
|
||||||
|
let insets = content_insets(&element.style);
|
||||||
if let Some(text) = element.text_node() {
|
if let Some(text) = element.text_node() {
|
||||||
let measured = text_system.measure_spans(
|
let measured = text_system.measure_spans(
|
||||||
&text.spans,
|
&text.spans,
|
||||||
@@ -470,37 +546,37 @@ fn intrinsic_size(
|
|||||||
Some(available_size.height.max(0.0)),
|
Some(available_size.height.max(0.0)),
|
||||||
);
|
);
|
||||||
return UiSize::new(
|
return UiSize::new(
|
||||||
element.style.width.unwrap_or(
|
element
|
||||||
measured.width + element.style.padding.left + element.style.padding.right,
|
.style
|
||||||
),
|
.width
|
||||||
element.style.height.unwrap_or(
|
.unwrap_or(measured.width + horizontal_insets(insets)),
|
||||||
measured.height + element.style.padding.top + element.style.padding.bottom,
|
element
|
||||||
),
|
.style
|
||||||
|
.height
|
||||||
|
.unwrap_or(measured.height + vertical_insets(insets)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(image) = element.image_node() {
|
if let Some(image) = element.image_node() {
|
||||||
let resolved = resolve_image_element_size(element, image.resource.size());
|
let resolved = resolve_image_element_size(element, image.resource.size());
|
||||||
return UiSize::new(
|
return UiSize::new(
|
||||||
resolved.width + element.style.padding.left + element.style.padding.right,
|
resolved.width + horizontal_insets(insets),
|
||||||
resolved.height + element.style.padding.top + element.style.padding.bottom,
|
resolved.height + vertical_insets(insets),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let explicit_width = element.style.width;
|
let explicit_width = element.style.width;
|
||||||
let explicit_height = element.style.height;
|
let explicit_height = element.style.height;
|
||||||
let content_width = explicit_width.unwrap_or(available_size.width).max(0.0)
|
let content_width =
|
||||||
- element.style.padding.left
|
explicit_width.unwrap_or(available_size.width).max(0.0) - horizontal_insets(insets);
|
||||||
- element.style.padding.right;
|
let content_height =
|
||||||
let content_height = explicit_height.unwrap_or(available_size.height).max(0.0)
|
explicit_height.unwrap_or(available_size.height).max(0.0) - vertical_insets(insets);
|
||||||
- element.style.padding.top
|
|
||||||
- element.style.padding.bottom;
|
|
||||||
let content_size = UiSize::new(content_width.max(0.0), content_height.max(0.0));
|
let content_size = UiSize::new(content_width.max(0.0), content_height.max(0.0));
|
||||||
|
|
||||||
if element.children.is_empty() {
|
if element.children.is_empty() {
|
||||||
return UiSize::new(
|
return UiSize::new(
|
||||||
explicit_width.unwrap_or(element.style.padding.left + element.style.padding.right),
|
explicit_width.unwrap_or(horizontal_insets(insets)),
|
||||||
explicit_height.unwrap_or(element.style.padding.top + element.style.padding.bottom),
|
explicit_height.unwrap_or(vertical_insets(insets)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,12 +658,8 @@ fn intrinsic_size(
|
|||||||
};
|
};
|
||||||
|
|
||||||
UiSize::new(
|
UiSize::new(
|
||||||
explicit_width.unwrap_or(
|
explicit_width.unwrap_or(intrinsic_content_width + horizontal_insets(insets)),
|
||||||
intrinsic_content_width + element.style.padding.left + element.style.padding.right,
|
explicit_height.unwrap_or(intrinsic_content_height + vertical_insets(insets)),
|
||||||
),
|
|
||||||
explicit_height.unwrap_or(
|
|
||||||
intrinsic_content_height + element.style.padding.top + element.style.padding.bottom,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,6 +759,150 @@ fn resolve_image_element_size(element: &Element, intrinsic: UiSize) -> UiSize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_box_shadows(
|
||||||
|
scene: &mut SceneSnapshot,
|
||||||
|
rect: Rect,
|
||||||
|
style: &Style,
|
||||||
|
kind: crate::BoxShadowKind,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
) {
|
||||||
|
let source_radius = uniform_corner_radius(style, rect);
|
||||||
|
for shadow in &style.box_shadows {
|
||||||
|
let shadow_rect = Rect::new(
|
||||||
|
rect.origin.x + shadow.offset.x - shadow.spread,
|
||||||
|
rect.origin.y + shadow.offset.y - shadow.spread,
|
||||||
|
rect.size.width + shadow.spread * 2.0,
|
||||||
|
rect.size.height + shadow.spread * 2.0,
|
||||||
|
);
|
||||||
|
if shadow.kind != kind || shadow_rect.size.width <= 0.0 || shadow_rect.size.height <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
perf_stats.background_quads += 1;
|
||||||
|
scene.push_shadow_rect(ShadowRect {
|
||||||
|
rect: shadow_rect,
|
||||||
|
source_rect: rect,
|
||||||
|
color: shadow.color,
|
||||||
|
blur: shadow.blur.max(0.0),
|
||||||
|
radius: clamp_corner_radius(source_radius + shadow.spread, shadow_rect),
|
||||||
|
source_radius,
|
||||||
|
kind,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_container_fill_and_border(
|
||||||
|
scene: &mut SceneSnapshot,
|
||||||
|
rect: Rect,
|
||||||
|
style: &Style,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
) {
|
||||||
|
let radius = uniform_corner_radius(style, rect);
|
||||||
|
let border_width = style
|
||||||
|
.border
|
||||||
|
.map(|border| {
|
||||||
|
border
|
||||||
|
.width
|
||||||
|
.clamp(0.0, rect.size.width * 0.5)
|
||||||
|
.min(rect.size.height * 0.5)
|
||||||
|
})
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let border_color = style.border.map(|border| border.color);
|
||||||
|
|
||||||
|
if radius > 0.0 && (style.background.is_some() || border_width > 0.0) {
|
||||||
|
perf_stats.background_quads += 1;
|
||||||
|
scene.push_rounded_rect(RoundedRect {
|
||||||
|
rect,
|
||||||
|
fill: style.background,
|
||||||
|
border_color,
|
||||||
|
border_width,
|
||||||
|
radius,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(color) = style.background {
|
||||||
|
perf_stats.background_quads += 1;
|
||||||
|
scene.push_quad(rect, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(border) = style.border else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if border_width <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let top = Rect::new(rect.origin.x, rect.origin.y, rect.size.width, border_width);
|
||||||
|
let bottom = Rect::new(
|
||||||
|
rect.origin.x,
|
||||||
|
rect.origin.y + rect.size.height - border_width,
|
||||||
|
rect.size.width,
|
||||||
|
border_width,
|
||||||
|
);
|
||||||
|
let middle_height = (rect.size.height - border_width * 2.0).max(0.0);
|
||||||
|
let left = Rect::new(
|
||||||
|
rect.origin.x,
|
||||||
|
rect.origin.y + border_width,
|
||||||
|
border_width,
|
||||||
|
middle_height,
|
||||||
|
);
|
||||||
|
let right = Rect::new(
|
||||||
|
rect.origin.x + rect.size.width - border_width,
|
||||||
|
rect.origin.y + border_width,
|
||||||
|
border_width,
|
||||||
|
middle_height,
|
||||||
|
);
|
||||||
|
|
||||||
|
for border_rect in [top, bottom, left, right] {
|
||||||
|
if border_rect.size.width <= 0.0 || border_rect.size.height <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
perf_stats.background_quads += 1;
|
||||||
|
scene.push_quad(border_rect, border.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_insets(style: &Style) -> Edges {
|
||||||
|
let border = style
|
||||||
|
.border
|
||||||
|
.map(|border| border.width.max(0.0))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
Edges {
|
||||||
|
top: style.padding.top + border,
|
||||||
|
right: style.padding.right + border,
|
||||||
|
bottom: style.padding.bottom + border,
|
||||||
|
left: style.padding.left + border,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uniform_corner_radius(style: &Style, rect: Rect) -> f32 {
|
||||||
|
clamp_corner_radius(
|
||||||
|
style
|
||||||
|
.corner_radius
|
||||||
|
.top_left
|
||||||
|
.max(style.corner_radius.top_right)
|
||||||
|
.max(style.corner_radius.bottom_right)
|
||||||
|
.max(style.corner_radius.bottom_left),
|
||||||
|
rect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_corner_radius(radius: f32, rect: Rect) -> f32 {
|
||||||
|
radius
|
||||||
|
.max(0.0)
|
||||||
|
.min(rect.size.width * 0.5)
|
||||||
|
.min(rect.size.height * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn horizontal_insets(edges: Edges) -> f32 {
|
||||||
|
edges.left + edges.right
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vertical_insets(edges: Edges) -> f32 {
|
||||||
|
edges.top + edges.bottom
|
||||||
|
}
|
||||||
|
|
||||||
fn inset_rect(rect: Rect, edges: Edges) -> Rect {
|
fn inset_rect(rect: Rect, edges: Edges) -> Rect {
|
||||||
let width = (rect.size.width - edges.left - edges.right).max(0.0);
|
let width = (rect.size.width - edges.left - edges.right).max(0.0);
|
||||||
let height = (rect.size.height - edges.top - edges.bottom).max(0.0);
|
let height = (rect.size.height - edges.top - edges.bottom).max(0.0);
|
||||||
@@ -891,6 +1107,43 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bordered_container_insets_text_and_emits_border_quads() {
|
||||||
|
let border_color = Color::rgb(0xF5, 0xD0, 0x74);
|
||||||
|
let root = Element::column().child(
|
||||||
|
Element::column()
|
||||||
|
.background(Color::rgb(0x22, 0x33, 0x44))
|
||||||
|
.border(4.0, border_color)
|
||||||
|
.padding(Edges::all(10.0))
|
||||||
|
.child(Element::paragraph(
|
||||||
|
"Bordered content should be inset past the stroke.",
|
||||||
|
TextStyle::new(18.0, Color::rgb(0xFF, 0xFF, 0xFF)).with_line_height(24.0),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let scene = layout_scene(1, UiSize::new(320.0, 160.0), &root);
|
||||||
|
let text = scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.find_map(|item| match item {
|
||||||
|
DisplayItem::Text(text) => Some(text),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("bordered container should emit text");
|
||||||
|
let border_quads: Vec<Quad> = scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
DisplayItem::Quad(quad) if quad.color == border_color => Some(*quad),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(text.origin.x, 14.0);
|
||||||
|
assert_eq!(text.origin.y, 14.0);
|
||||||
|
assert_eq!(border_quads.len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn row_container_counts_flex_child_height_in_intrinsic_size() {
|
fn row_container_counts_flex_child_height_in_intrinsic_size() {
|
||||||
let row_color = Color::rgb(0x12, 0x24, 0x36);
|
let row_color = Color::rgb(0x12, 0x24, 0x36);
|
||||||
@@ -945,6 +1198,78 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rounded_container_emits_rounded_rect_and_clip_items() {
|
||||||
|
let root = Element::column().child(
|
||||||
|
Element::column()
|
||||||
|
.background(Color::rgb(0x18, 0x20, 0x2F))
|
||||||
|
.border(2.0, Color::rgb(0xF5, 0xD0, 0x74))
|
||||||
|
.corner_radius(18.0)
|
||||||
|
.padding(Edges::all(12.0))
|
||||||
|
.child(Element::paragraph(
|
||||||
|
"Rounded containers should emit a rounded paint item and clip their children.",
|
||||||
|
TextStyle::new(18.0, Color::rgb(0xFF, 0xFF, 0xFF)).with_line_height(24.0),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let scene = layout_scene(1, UiSize::new(320.0, 180.0), &root);
|
||||||
|
assert!(
|
||||||
|
scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|item| matches!(item, DisplayItem::RoundedRect(_)))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|item| matches!(item, DisplayItem::PushClip(_)))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|item| matches!(item, DisplayItem::PopClip))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadowed_container_emits_shadow_rect_items() {
|
||||||
|
let root = Element::column().child(
|
||||||
|
Element::column()
|
||||||
|
.background(Color::rgb(0x18, 0x20, 0x2F))
|
||||||
|
.corner_radius(18.0)
|
||||||
|
.shadow(crate::BoxShadow::new(
|
||||||
|
crate::Point::new(0.0, 8.0),
|
||||||
|
18.0,
|
||||||
|
-2.0,
|
||||||
|
Color::rgba(0x05, 0x08, 0x0F, 0x90),
|
||||||
|
crate::BoxShadowKind::Outer,
|
||||||
|
))
|
||||||
|
.shadow(crate::BoxShadow::new(
|
||||||
|
crate::Point::new(0.0, 4.0),
|
||||||
|
8.0,
|
||||||
|
0.0,
|
||||||
|
Color::rgba(0x00, 0x00, 0x00, 0x70),
|
||||||
|
crate::BoxShadowKind::Inner,
|
||||||
|
))
|
||||||
|
.child(Element::paragraph(
|
||||||
|
"Shadowed containers should emit dedicated shadow paint items.",
|
||||||
|
TextStyle::new(18.0, Color::rgb(0xFF, 0xFF, 0xFF)).with_line_height(24.0),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let scene = layout_scene(1, UiSize::new(320.0, 180.0), &root);
|
||||||
|
assert!(
|
||||||
|
scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| matches!(item, DisplayItem::ShadowRect(_)))
|
||||||
|
.count()
|
||||||
|
>= 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interaction_tree_hit_test_returns_deepest_pointer_target() {
|
fn interaction_tree_hit_test_returns_deepest_pointer_target() {
|
||||||
let root = Element::column()
|
let root = Element::column()
|
||||||
@@ -995,6 +1320,31 @@ mod tests {
|
|||||||
assert_eq!(hit.element_id, Some(ElementId::new(2)));
|
assert_eq!(hit.element_id, Some(ElementId::new(2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rounded_corner_hit_test_excludes_clipped_corner_region() {
|
||||||
|
let root = Element::column().pointer_events(false).child(
|
||||||
|
Element::column()
|
||||||
|
.id(ElementId::new(2))
|
||||||
|
.width(120.0)
|
||||||
|
.height(80.0)
|
||||||
|
.corner_radius(20.0)
|
||||||
|
.background(Color::rgb(0x22, 0x33, 0x44)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = layout_snapshot(1, UiSize::new(160.0, 120.0), &root);
|
||||||
|
assert!(
|
||||||
|
snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.hit_test(Point::new(4.0, 4.0))
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
let hit = snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.hit_test(Point::new(24.0, 24.0))
|
||||||
|
.expect("point inside rounded body should hit child");
|
||||||
|
assert_eq!(hit.element_id, Some(ElementId::new(2)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interaction_tree_hit_path_includes_pointer_ancestors() {
|
fn interaction_tree_hit_path_includes_pointer_ancestors() {
|
||||||
let root = Element::column().pointer_events(false).child(
|
let root = Element::column().pointer_events(false).child(
|
||||||
|
|||||||
@@ -38,14 +38,17 @@ pub use platform::{
|
|||||||
};
|
};
|
||||||
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
|
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
|
||||||
pub use scene::{
|
pub use scene::{
|
||||||
Color, DisplayItem, GlyphInstance, Point, PreparedImage, PreparedText, PreparedTextLine, Quad,
|
ClipRegion, Color, DisplayItem, GlyphInstance, Point, PreparedImage, PreparedText,
|
||||||
Rect, SceneSnapshot, Translation, UiSize,
|
PreparedTextLine, Quad, Rect, RoundedRect, SceneSnapshot, ShadowRect, Translation, UiSize,
|
||||||
};
|
};
|
||||||
pub use text::{
|
pub use text::{
|
||||||
TextAlign, TextFontFamily, TextSelectionStyle, TextSpan, TextSpanSlant, TextSpanWeight,
|
TextAlign, TextFontFamily, TextSelectionStyle, TextSpan, TextSpanSlant, TextSpanWeight,
|
||||||
TextStyle, TextSystem, TextWrap,
|
TextStyle, TextSystem, TextWrap,
|
||||||
};
|
};
|
||||||
pub use tree::{CursorIcon, Edges, Element, ElementId, FlexDirection, Style};
|
pub use tree::{
|
||||||
|
Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element, ElementId,
|
||||||
|
FlexDirection, Style,
|
||||||
|
};
|
||||||
pub use window::{
|
pub use window::{
|
||||||
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use tracing::debug;
|
|||||||
use crate::ImageResource;
|
use crate::ImageResource;
|
||||||
use crate::text::TextSelectionStyle;
|
use crate::text::TextSelectionStyle;
|
||||||
use crate::trace_targets;
|
use crate::trace_targets;
|
||||||
use crate::tree::ElementId;
|
use crate::tree::{BoxShadowKind, ElementId};
|
||||||
|
|
||||||
pub type SceneVersion = u64;
|
pub type SceneVersion = u64;
|
||||||
|
|
||||||
@@ -100,6 +100,32 @@ impl Quad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct RoundedRect {
|
||||||
|
pub rect: Rect,
|
||||||
|
pub fill: Option<Color>,
|
||||||
|
pub border_color: Option<Color>,
|
||||||
|
pub border_width: f32,
|
||||||
|
pub radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ShadowRect {
|
||||||
|
pub rect: Rect,
|
||||||
|
pub source_rect: Rect,
|
||||||
|
pub color: Color,
|
||||||
|
pub blur: f32,
|
||||||
|
pub radius: f32,
|
||||||
|
pub source_radius: f32,
|
||||||
|
pub kind: BoxShadowKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ClipRegion {
|
||||||
|
pub rect: Rect,
|
||||||
|
pub radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct GlyphInstance {
|
pub struct GlyphInstance {
|
||||||
pub position: Point,
|
pub position: Point,
|
||||||
@@ -465,9 +491,11 @@ fn classify_word_char(ch: char) -> WordClass {
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum DisplayItem {
|
pub enum DisplayItem {
|
||||||
Quad(Quad),
|
Quad(Quad),
|
||||||
|
RoundedRect(RoundedRect),
|
||||||
|
ShadowRect(ShadowRect),
|
||||||
Image(PreparedImage),
|
Image(PreparedImage),
|
||||||
Text(PreparedText),
|
Text(PreparedText),
|
||||||
PushClip(Rect),
|
PushClip(ClipRegion),
|
||||||
PopClip,
|
PopClip,
|
||||||
PushTransform(Translation),
|
PushTransform(Translation),
|
||||||
PopTransform,
|
PopTransform,
|
||||||
@@ -512,6 +540,14 @@ impl SceneSnapshot {
|
|||||||
self.push_item(DisplayItem::Quad(Quad::new(rect, color)))
|
self.push_item(DisplayItem::Quad(Quad::new(rect, color)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_rounded_rect(&mut self, rounded_rect: RoundedRect) -> &mut Self {
|
||||||
|
self.push_item(DisplayItem::RoundedRect(rounded_rect))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_shadow_rect(&mut self, shadow_rect: ShadowRect) -> &mut Self {
|
||||||
|
self.push_item(DisplayItem::ShadowRect(shadow_rect))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push_text(&mut self, text: PreparedText) -> &mut Self {
|
pub fn push_text(&mut self, text: PreparedText) -> &mut Self {
|
||||||
self.push_item(DisplayItem::Text(text))
|
self.push_item(DisplayItem::Text(text))
|
||||||
}
|
}
|
||||||
@@ -520,8 +556,8 @@ impl SceneSnapshot {
|
|||||||
self.push_item(DisplayItem::Image(image))
|
self.push_item(DisplayItem::Image(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_clip(&mut self, rect: Rect) -> &mut Self {
|
pub fn push_clip(&mut self, rect: Rect, radius: f32) -> &mut Self {
|
||||||
self.push_item(DisplayItem::PushClip(rect))
|
self.push_item(DisplayItem::PushClip(ClipRegion { rect, radius }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop_clip(&mut self) -> &mut Self {
|
pub fn pop_clip(&mut self) -> &mut Self {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::scene::Color;
|
use crate::scene::{Color, Point};
|
||||||
use crate::text::{TextSpan, TextStyle, TextWrap};
|
use crate::text::{TextSpan, TextStyle, TextWrap};
|
||||||
use crate::{ImageFit, ImageResource};
|
use crate::{ImageFit, ImageResource};
|
||||||
|
|
||||||
@@ -63,6 +63,77 @@ impl Edges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Border {
|
||||||
|
pub width: f32,
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Border {
|
||||||
|
pub const fn new(width: f32, color: Color) -> Self {
|
||||||
|
Self { width, color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct CornerRadius {
|
||||||
|
pub top_left: f32,
|
||||||
|
pub top_right: f32,
|
||||||
|
pub bottom_right: f32,
|
||||||
|
pub bottom_left: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CornerRadius {
|
||||||
|
pub const ZERO: Self = Self {
|
||||||
|
top_left: 0.0,
|
||||||
|
top_right: 0.0,
|
||||||
|
bottom_right: 0.0,
|
||||||
|
bottom_left: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn all(radius: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: radius,
|
||||||
|
top_right: radius,
|
||||||
|
bottom_right: radius,
|
||||||
|
bottom_left: radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum BoxShadowKind {
|
||||||
|
Outer,
|
||||||
|
Inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct BoxShadow {
|
||||||
|
pub offset: Point,
|
||||||
|
pub blur: f32,
|
||||||
|
pub spread: f32,
|
||||||
|
pub color: Color,
|
||||||
|
pub kind: BoxShadowKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxShadow {
|
||||||
|
pub const fn new(
|
||||||
|
offset: Point,
|
||||||
|
blur: f32,
|
||||||
|
spread: f32,
|
||||||
|
color: Color,
|
||||||
|
kind: BoxShadowKind,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
offset,
|
||||||
|
blur,
|
||||||
|
spread,
|
||||||
|
color,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
pub direction: FlexDirection,
|
pub direction: FlexDirection,
|
||||||
@@ -72,6 +143,9 @@ pub struct Style {
|
|||||||
pub gap: f32,
|
pub gap: f32,
|
||||||
pub padding: Edges,
|
pub padding: Edges,
|
||||||
pub background: Option<Color>,
|
pub background: Option<Color>,
|
||||||
|
pub border: Option<Border>,
|
||||||
|
pub corner_radius: CornerRadius,
|
||||||
|
pub box_shadows: Vec<BoxShadow>,
|
||||||
pub pointer_events: bool,
|
pub pointer_events: bool,
|
||||||
pub focusable: bool,
|
pub focusable: bool,
|
||||||
pub cursor: Option<CursorIcon>,
|
pub cursor: Option<CursorIcon>,
|
||||||
@@ -87,6 +161,9 @@ impl Default for Style {
|
|||||||
gap: 0.0,
|
gap: 0.0,
|
||||||
padding: Edges::ZERO,
|
padding: Edges::ZERO,
|
||||||
background: None,
|
background: None,
|
||||||
|
border: None,
|
||||||
|
corner_radius: CornerRadius::ZERO,
|
||||||
|
box_shadows: Vec::new(),
|
||||||
pointer_events: true,
|
pointer_events: true,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
@@ -210,6 +287,21 @@ impl Element {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn border(mut self, width: f32, color: Color) -> Self {
|
||||||
|
self.style.border = Some(Border::new(width.max(0.0), color));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn corner_radius(mut self, radius: f32) -> Self {
|
||||||
|
self.style.corner_radius = CornerRadius::all(radius.max(0.0));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shadow(mut self, shadow: BoxShadow) -> Self {
|
||||||
|
self.style.box_shadows.push(shadow);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn id(mut self, id: ElementId) -> Self {
|
pub fn id(mut self, id: ElementId) -> Self {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
self
|
self
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user