727 lines
21 KiB
Rust
727 lines
21 KiB
Rust
use crate::elements::Fill;
|
|
use crate::geometry::vector::vec2f;
|
|
use crate::image_cache::StaticImage;
|
|
use crate::{
|
|
elements::Point,
|
|
fonts::{FontId, GlyphId},
|
|
rendering,
|
|
};
|
|
use ordered_float::OrderedFloat;
|
|
use pathfinder_color::ColorU;
|
|
use pathfinder_geometry::rect::RectF;
|
|
use pathfinder_geometry::vector::Vector2F;
|
|
use rstar::{primitives::Rectangle, RTree};
|
|
use std::sync::Arc;
|
|
use vec1::{vec1, Vec1};
|
|
|
|
#[derive(Clone)]
|
|
pub struct Scene {
|
|
scale_factor: f32,
|
|
rendering_config: rendering::Config,
|
|
active_layer_index_stack: Vec1<ZIndex>,
|
|
layers: Vec1<Layer>,
|
|
overlay_layers: Vec<Layer>,
|
|
#[cfg(debug_assertions)]
|
|
/// Custom panic location, set with [`Scene::set_location_for_panic_logging`]
|
|
panic_location: Option<&'static std::panic::Location<'static>>,
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct Layer {
|
|
hit_map: RTree<Rectangle<[OrderedFloat<f32>; 2]>>,
|
|
pub clip_bounds: Option<RectF>,
|
|
pub rects: Vec<Rect>,
|
|
pub images: Vec<Image>,
|
|
pub glyphs: Vec<Glyph>,
|
|
pub icons: Vec<Icon>,
|
|
pub click_through: bool,
|
|
}
|
|
|
|
/// Clip bounds to use for a layer.
|
|
pub enum ClipBounds {
|
|
/// Use the bounds of the active layer.
|
|
ActiveLayer,
|
|
/// Use the specified bounds as the bounds for the new layer.
|
|
///
|
|
/// Note that this ignores any clip bounds applied to the currently-active
|
|
/// layer.
|
|
BoundedBy(RectF),
|
|
/// Intersect the active layer's bounds and the provided rect
|
|
/// to get the bounds for the new layer.
|
|
BoundedByActiveLayerAnd(RectF),
|
|
/// No clipping
|
|
None,
|
|
}
|
|
|
|
impl Layer {
|
|
fn record_hit_rect(&mut self, rect: RectF) {
|
|
if let Some(intersected) = self
|
|
.clip_bounds
|
|
.map_or(Some(rect), |c| rect.intersection(c))
|
|
{
|
|
self.hit_map.insert(Rectangle::from_corners(
|
|
[intersected.min_x().into(), intersected.min_y().into()],
|
|
[intersected.max_x().into(), intersected.max_y().into()],
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
|
pub struct GlyphKey {
|
|
pub glyph_id: GlyphId,
|
|
pub font_id: FontId,
|
|
pub font_size: OrderedFloat<f32>,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum GlyphFade {
|
|
/// A horizontal fade from alpha 1 to 0 with start and end positions in screen coordinates
|
|
/// start - where the fade is transparent
|
|
/// end - where the fade is most opaque
|
|
Horizontal { start: f32, end: f32 },
|
|
}
|
|
|
|
impl GlyphFade {
|
|
pub fn horizontal(start: f32, end: f32) -> Self {
|
|
GlyphFade::Horizontal { start, end }
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Glyph {
|
|
pub glyph_key: GlyphKey,
|
|
pub position: Vector2F,
|
|
pub fade: Option<GlyphFade>,
|
|
pub color: ColorU,
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct Rect {
|
|
pub bounds: RectF,
|
|
pub drop_shadow: Option<DropShadow>,
|
|
pub corner_radius: CornerRadius,
|
|
pub background: Fill,
|
|
pub border: Border,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Image {
|
|
pub bounds: RectF,
|
|
pub asset: Arc<StaticImage>,
|
|
pub opacity: f32,
|
|
pub corner_radius: CornerRadius,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Icon {
|
|
pub bounds: RectF,
|
|
pub asset: Arc<StaticImage>,
|
|
pub opacity: f32,
|
|
pub color: ColorU,
|
|
}
|
|
|
|
// These were picked empirically to make the shadows look decent by
|
|
// default, but there is nothing special about them.
|
|
const DEFAULT_DROP_SHADOW_OFFSET_X: f32 = 0.;
|
|
const DEFAULT_DROP_SHADOW_OFFSET_Y: f32 = 10.;
|
|
const DEFAULT_DROP_SHADOW_BLUR_RADIUS: f32 = 10.;
|
|
const DEFAULT_DROP_SHADOW_SPREAD_RADIUS: f32 = 30.;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct DropShadow {
|
|
pub color: ColorU,
|
|
|
|
// How the shadow is offset from the target rect
|
|
pub offset: Vector2F,
|
|
|
|
// Controls how tightly sampled the shadow is - the larger the number
|
|
// the more spread out the shadow.
|
|
pub blur_radius: f32,
|
|
|
|
// Controls how wide the shadow is outside the target.
|
|
pub spread_radius: f32,
|
|
}
|
|
|
|
impl DropShadow {
|
|
pub fn new_with_standard_offset_and_spread(color: ColorU) -> Self {
|
|
Self {
|
|
color,
|
|
offset: vec2f(DEFAULT_DROP_SHADOW_OFFSET_X, DEFAULT_DROP_SHADOW_OFFSET_Y),
|
|
blur_radius: DEFAULT_DROP_SHADOW_BLUR_RADIUS,
|
|
spread_radius: DEFAULT_DROP_SHADOW_SPREAD_RADIUS,
|
|
}
|
|
}
|
|
|
|
pub fn with_offset(mut self, offset: Vector2F) -> Self {
|
|
self.offset = offset;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Default for DropShadow {
|
|
fn default() -> Self {
|
|
Self::new_with_standard_offset_and_spread(ColorU::new(0, 0, 0, 32))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
|
pub struct Border {
|
|
pub width: f32,
|
|
pub color: Fill,
|
|
pub top: bool,
|
|
pub left: bool,
|
|
pub bottom: bool,
|
|
pub right: bool,
|
|
pub dash: Option<Dash>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
|
pub struct Dash {
|
|
pub dash_length: f32,
|
|
pub gap_length: f32,
|
|
|
|
/// If true, gaps will always be the length specified in `gap_length`.
|
|
/// Otherwise, gap length may be adjusted slightly to guarantee that the
|
|
/// dashed line starts and ends with a dash.
|
|
pub force_consistent_gap_length: bool,
|
|
}
|
|
|
|
impl Border {
|
|
pub fn top_width(&self) -> f32 {
|
|
if self.top {
|
|
self.width
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
pub fn right_width(&self) -> f32 {
|
|
if self.right {
|
|
self.width
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
pub fn bottom_width(&self) -> f32 {
|
|
if self.bottom {
|
|
self.width
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
pub fn left_width(&self) -> f32 {
|
|
if self.left {
|
|
self.width
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum Radius {
|
|
/// Specify a radius in absolute pixels.
|
|
Pixels(f32),
|
|
/// Specify a radius as a percentage of the rectangle's smaller dimension.
|
|
/// For example, using `Percentage(50.)` will produce a pill shape.
|
|
Percentage(f32),
|
|
}
|
|
|
|
impl Default for Radius {
|
|
fn default() -> Self {
|
|
Radius::Pixels(0.)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
pub struct CornerRadius {
|
|
/// Top left corner radius
|
|
top_left: Option<Radius>,
|
|
/// Top right corner radius
|
|
top_right: Option<Radius>,
|
|
/// Bottom left corner radius
|
|
bottom_left: Option<Radius>,
|
|
/// Bottom right corner radius
|
|
bottom_right: Option<Radius>,
|
|
}
|
|
|
|
impl CornerRadius {
|
|
/// Merge this CornerRadius struct with another.
|
|
/// `Some(r)` takes precedence over `None`.
|
|
/// If both are present, `other`'s values, take precedence over `self`'s existing values.
|
|
pub fn merge(&mut self, other: CornerRadius) {
|
|
self.top_left = other.top_left.or(self.top_left);
|
|
self.top_right = other.top_right.or(self.top_right);
|
|
self.bottom_left = other.bottom_left.or(self.bottom_left);
|
|
self.bottom_right = other.bottom_right.or(self.bottom_right);
|
|
}
|
|
|
|
pub fn get_top_left(&self) -> Radius {
|
|
self.top_left.unwrap_or(Radius::Pixels(0.))
|
|
}
|
|
|
|
pub fn get_top_right(&self) -> Radius {
|
|
self.top_right.unwrap_or(Radius::Pixels(0.))
|
|
}
|
|
|
|
pub fn get_bottom_left(&self) -> Radius {
|
|
self.bottom_left.unwrap_or(Radius::Pixels(0.))
|
|
}
|
|
|
|
pub fn get_bottom_right(&self) -> Radius {
|
|
self.bottom_right.unwrap_or(Radius::Pixels(0.))
|
|
}
|
|
|
|
pub const fn with_all(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: Some(radius),
|
|
top_right: Some(radius),
|
|
bottom_left: Some(radius),
|
|
bottom_right: Some(radius),
|
|
}
|
|
}
|
|
pub const fn with_top(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: Some(radius),
|
|
top_right: Some(radius),
|
|
bottom_left: None,
|
|
bottom_right: None,
|
|
}
|
|
}
|
|
pub const fn with_bottom(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: None,
|
|
top_right: None,
|
|
bottom_left: Some(radius),
|
|
bottom_right: Some(radius),
|
|
}
|
|
}
|
|
pub const fn with_left(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: Some(radius),
|
|
top_right: None,
|
|
bottom_left: Some(radius),
|
|
bottom_right: None,
|
|
}
|
|
}
|
|
pub const fn with_right(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: None,
|
|
top_right: Some(radius),
|
|
bottom_left: None,
|
|
bottom_right: Some(radius),
|
|
}
|
|
}
|
|
pub const fn with_top_left(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: Some(radius),
|
|
top_right: None,
|
|
bottom_left: None,
|
|
bottom_right: None,
|
|
}
|
|
}
|
|
pub const fn with_top_right(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: None,
|
|
top_right: Some(radius),
|
|
bottom_left: None,
|
|
bottom_right: None,
|
|
}
|
|
}
|
|
pub const fn with_bottom_left(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: None,
|
|
top_right: None,
|
|
bottom_left: Some(radius),
|
|
bottom_right: None,
|
|
}
|
|
}
|
|
pub const fn with_bottom_right(radius: Radius) -> Self {
|
|
CornerRadius {
|
|
top_left: None,
|
|
top_right: None,
|
|
bottom_left: None,
|
|
bottom_right: Some(radius),
|
|
}
|
|
}
|
|
|
|
/// Filters this [`CornerRadius`] to only have the top corners rounded.
|
|
pub const fn top(self) -> Self {
|
|
CornerRadius {
|
|
top_left: self.top_left,
|
|
top_right: self.top_right,
|
|
bottom_left: None,
|
|
bottom_right: None,
|
|
}
|
|
}
|
|
|
|
/// Filters this [`CornerRadius`] to only have the bottom corners rounded.
|
|
pub const fn bottom(self) -> Self {
|
|
CornerRadius {
|
|
top_left: None,
|
|
top_right: None,
|
|
bottom_left: self.bottom_left,
|
|
bottom_right: self.bottom_right,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
/// Newtype to encapsulate a Z index, which actually represents a layer index in the list of layers
|
|
pub enum ZIndex {
|
|
Normal(usize),
|
|
Overlay(usize),
|
|
}
|
|
|
|
impl ZIndex {
|
|
#[cfg(test)]
|
|
pub fn new(layer: usize) -> Self {
|
|
ZIndex::Normal(layer)
|
|
}
|
|
}
|
|
|
|
impl Scene {
|
|
pub fn new(scale_factor: f32, rendering_config: rendering::Config) -> Self {
|
|
Self {
|
|
scale_factor,
|
|
rendering_config,
|
|
active_layer_index_stack: vec1![ZIndex::Normal(0)],
|
|
layers: vec1![Layer::default()],
|
|
overlay_layers: Vec::new(),
|
|
#[cfg(debug_assertions)]
|
|
panic_location: None,
|
|
}
|
|
}
|
|
|
|
/// Temporarily set the panic location for the scene. This is cleared
|
|
/// during the next draw call.
|
|
#[cfg(debug_assertions)]
|
|
pub fn set_location_for_panic_logging(
|
|
&mut self,
|
|
panic_location: Option<&'static std::panic::Location<'static>>,
|
|
) {
|
|
self.panic_location = panic_location;
|
|
}
|
|
|
|
fn active_layer(&mut self) -> &mut Layer {
|
|
match *self.active_layer_index_stack.last() {
|
|
ZIndex::Normal(index) => &mut self.layers[index],
|
|
ZIndex::Overlay(index) => &mut self.overlay_layers[index],
|
|
}
|
|
}
|
|
|
|
pub fn is_covered(&self, position: Point) -> bool {
|
|
// Does any layer at a higher z-index contain this point?
|
|
let point = [position.x().into(), position.y().into()];
|
|
let predicate = |l: &Layer| !l.click_through && l.hit_map.locate_at_point(&point).is_some();
|
|
|
|
match position.z_index() {
|
|
ZIndex::Normal(index) => self
|
|
.layers
|
|
.get((index + 1)..)
|
|
.into_iter()
|
|
.flatten()
|
|
.chain(self.overlay_layers.iter())
|
|
.any(predicate),
|
|
ZIndex::Overlay(index) => self
|
|
.overlay_layers
|
|
.get((index + 1)..)
|
|
.into_iter()
|
|
.flatten()
|
|
.any(predicate),
|
|
}
|
|
}
|
|
|
|
// Compute the intersection between the bound of the element and the clip bound
|
|
// on its current layer. The intersection is then checked against the event position
|
|
// to determine whether we should dispatch the event.
|
|
pub fn visible_rect(&self, origin: Point, size: Vector2F) -> Option<RectF> {
|
|
// TODO: Investigate how / when we would pass a z-index that isn't in the scene
|
|
// This appears to be fairly common, based on adding sentry reporting to it, however it
|
|
// doesn't seem to dramatically impact app usage. Perhaps it's something that happens on
|
|
// a view teardown frame?
|
|
let maybe_layer = match origin.z_index() {
|
|
ZIndex::Normal(index) => self.layers.get(index),
|
|
ZIndex::Overlay(index) => self.overlay_layers.get(index),
|
|
};
|
|
let maybe_bounds = maybe_layer.and_then(|layer| layer.clip_bounds);
|
|
|
|
let input_rect = RectF::new(origin.xy(), size);
|
|
match maybe_bounds {
|
|
Some(clip_rect) => clip_rect.intersection(input_rect),
|
|
None => Some(input_rect),
|
|
}
|
|
}
|
|
|
|
/// Get the Z-Index of the currently-active layer
|
|
pub fn z_index(&self) -> ZIndex {
|
|
*self.active_layer_index_stack.last()
|
|
}
|
|
|
|
/// Get the maximum Z-Index in the active layer stack (whether Normal or Overlay).
|
|
pub fn max_active_z_index(&self) -> ZIndex {
|
|
match self.active_layer_index_stack.last() {
|
|
ZIndex::Normal(_) => ZIndex::Normal(self.layers.len() - 1),
|
|
// Safety: If the active layer is an overlay layer, then there must be at least one
|
|
// overlay layer, so subtracting one from the length is valid.
|
|
ZIndex::Overlay(_) => ZIndex::Overlay(self.overlay_layers.len() - 1),
|
|
}
|
|
}
|
|
|
|
pub fn start_layer(&mut self, bounds: ClipBounds) {
|
|
let layer = self.create_layer(bounds);
|
|
|
|
match *self.active_layer_index_stack.last() {
|
|
ZIndex::Normal(_) => self.push_normal_layer(layer),
|
|
ZIndex::Overlay(_) => self.push_overlay_layer(layer),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn start_overlay_layer(&mut self, bounds: ClipBounds) {
|
|
let layer = self.create_layer(bounds);
|
|
self.push_overlay_layer(layer);
|
|
}
|
|
|
|
fn create_layer(&mut self, bounds: ClipBounds) -> Layer {
|
|
let clip_bounds = match bounds {
|
|
ClipBounds::ActiveLayer => self.active_layer().clip_bounds,
|
|
ClipBounds::BoundedBy(bounds) => Some(bounds),
|
|
ClipBounds::BoundedByActiveLayerAnd(bounds) => {
|
|
if let Some(current_layer_bounds) = self.active_layer().clip_bounds {
|
|
// If the current layer has bounds, return the intersection...
|
|
current_layer_bounds
|
|
.intersection(bounds)
|
|
// ...or, if the regions don't overlap, an empty bounding rect.
|
|
.or(Some(RectF::default()))
|
|
} else {
|
|
// If the current layer has no bounds, return the bounds
|
|
// for the new layer.
|
|
Some(bounds)
|
|
}
|
|
}
|
|
ClipBounds::None => None,
|
|
};
|
|
|
|
Layer {
|
|
clip_bounds,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn push_normal_layer(&mut self, layer: Layer) {
|
|
self.active_layer_index_stack
|
|
.push(ZIndex::Normal(self.layers.len()));
|
|
self.layers.push(layer);
|
|
}
|
|
|
|
fn push_overlay_layer(&mut self, layer: Layer) {
|
|
self.active_layer_index_stack
|
|
.push(ZIndex::Overlay(self.overlay_layers.len()));
|
|
self.overlay_layers.push(layer);
|
|
}
|
|
|
|
pub fn set_active_layer_click_through(&mut self) {
|
|
self.active_layer().click_through = true;
|
|
}
|
|
|
|
pub fn stop_layer(&mut self) {
|
|
if self.active_layer_index_stack.pop().is_err() {
|
|
panic!("popped the last layer from active_layer_index_stack");
|
|
}
|
|
}
|
|
|
|
fn validate_rect(rect: &RectF, location: Option<&'static std::panic::Location<'static>>) {
|
|
#[cfg(debug_assertions)]
|
|
let location_info = location
|
|
.map(|loc| {
|
|
format!(
|
|
" (element created at {}:{}:{})",
|
|
loc.file(),
|
|
loc.line(),
|
|
loc.column()
|
|
)
|
|
})
|
|
.unwrap_or_default();
|
|
#[cfg(not(debug_assertions))]
|
|
let location_info = "";
|
|
debug_assert!(
|
|
!rect.origin().y().is_infinite(),
|
|
"!rect.origin().y().is_infinite(){location_info}"
|
|
);
|
|
debug_assert!(
|
|
!rect.origin().y().is_nan(),
|
|
"!rect.origin().y().is_nan(){location_info}"
|
|
);
|
|
|
|
debug_assert!(
|
|
!rect.size().x().is_infinite(),
|
|
"!rect.size().x().is_infinite(){location_info}"
|
|
);
|
|
debug_assert!(
|
|
!rect.size().x().is_nan(),
|
|
"!rect.size().x().is_nan(){location_info}"
|
|
);
|
|
debug_assert!(
|
|
!rect.size().y().is_infinite(),
|
|
"!rect.size().y().is_infinite(){location_info}"
|
|
);
|
|
debug_assert!(
|
|
!rect.size().y().is_nan(),
|
|
"!rect.size().y().is_nan(){location_info}"
|
|
);
|
|
}
|
|
|
|
/// This method draws a rectangle without recording any information about it in the current
|
|
/// layer. Note this should be used with caution. In most cases, what you need is
|
|
/// `draw_rect_with_hit_recording` instead. However, in rare cases this may be useful for
|
|
/// performance reasons when many intermediate rects are drawn. If this is called, it is up to
|
|
/// the caller to also draw a rect (via draw_rect_with_hit_recording) that encompasses the range
|
|
/// of the rects drawn so that layer recording for event dispatching is correctly kept
|
|
/// up-to-date.
|
|
pub fn draw_rect_without_hit_recording(&mut self, rect: RectF) -> &mut Rect {
|
|
#[cfg(debug_assertions)]
|
|
let location = self.panic_location.take();
|
|
#[cfg(not(debug_assertions))]
|
|
let location = None;
|
|
let layer = self.active_layer();
|
|
Self::validate_rect(&rect, location);
|
|
|
|
layer.rects.push(Rect {
|
|
bounds: rect,
|
|
..Default::default()
|
|
});
|
|
layer.rects.last_mut().unwrap()
|
|
}
|
|
|
|
pub fn draw_rect_with_hit_recording(&mut self, rect: RectF) -> &mut Rect {
|
|
let layer = self.active_layer();
|
|
layer.record_hit_rect(rect);
|
|
self.draw_rect_without_hit_recording(rect)
|
|
}
|
|
|
|
pub fn draw_image(
|
|
&mut self,
|
|
rect: RectF,
|
|
asset: Arc<StaticImage>,
|
|
opacity: f32,
|
|
corner_radius: CornerRadius,
|
|
) {
|
|
#[cfg(debug_assertions)]
|
|
let location = self.panic_location.take();
|
|
#[cfg(not(debug_assertions))]
|
|
let location = None;
|
|
let layer = self.active_layer();
|
|
Self::validate_rect(&rect, location);
|
|
|
|
layer.images.push(Image {
|
|
bounds: rect,
|
|
asset,
|
|
opacity,
|
|
corner_radius,
|
|
});
|
|
layer.record_hit_rect(rect);
|
|
}
|
|
|
|
pub fn draw_icon(&mut self, rect: RectF, asset: Arc<StaticImage>, opacity: f32, color: ColorU) {
|
|
#[cfg(debug_assertions)]
|
|
let location = self.panic_location.take();
|
|
#[cfg(not(debug_assertions))]
|
|
let location = None;
|
|
let layer = self.active_layer();
|
|
Self::validate_rect(&rect, location);
|
|
|
|
layer.icons.push(Icon {
|
|
bounds: rect,
|
|
asset,
|
|
opacity,
|
|
color,
|
|
});
|
|
layer.record_hit_rect(rect);
|
|
}
|
|
|
|
/// Adds a glyph that should be drawn in the scene.
|
|
///
|
|
/// `position` is the point at which the glyph's left edge meets the
|
|
/// baseline.
|
|
pub fn draw_glyph(
|
|
&mut self,
|
|
position: Vector2F,
|
|
glyph_id: GlyphId,
|
|
font_id: FontId,
|
|
font_size: f32,
|
|
color: ColorU,
|
|
) -> &mut Glyph {
|
|
// TODO: Support hit testing on glyphs?
|
|
let layer = self.active_layer();
|
|
layer.glyphs.push(Glyph {
|
|
glyph_key: GlyphKey {
|
|
glyph_id,
|
|
font_id,
|
|
font_size: font_size.into(),
|
|
},
|
|
position,
|
|
color,
|
|
fade: None,
|
|
});
|
|
layer.glyphs.last_mut().unwrap()
|
|
}
|
|
|
|
/// Get an iterator over all layers in order, from bottom to top
|
|
pub fn layers(&self) -> impl Iterator<Item = &Layer> {
|
|
self.layers.iter().chain(self.overlay_layers.iter())
|
|
}
|
|
|
|
/// Get the total number of layers
|
|
#[cfg(test)]
|
|
pub fn layer_count(&self) -> usize {
|
|
self.layers.len() + self.overlay_layers.len()
|
|
}
|
|
|
|
pub fn scale_factor(&self) -> f32 {
|
|
self.scale_factor
|
|
}
|
|
|
|
pub fn rendering_config(&self) -> &rendering::Config {
|
|
&self.rendering_config
|
|
}
|
|
}
|
|
|
|
impl Rect {
|
|
pub fn with_corner_radius(&mut self, radius: CornerRadius) -> &mut Self {
|
|
self.corner_radius.merge(radius);
|
|
self
|
|
}
|
|
|
|
pub fn with_border(&mut self, border: Border) -> &mut Self {
|
|
self.border = border;
|
|
self
|
|
}
|
|
|
|
pub fn with_background<F>(&mut self, background: F) -> &mut Self
|
|
where
|
|
F: Into<Fill>,
|
|
{
|
|
self.background = background.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_drop_shadow(&mut self, drop_shadow: DropShadow) -> &mut Self {
|
|
self.drop_shadow = Some(drop_shadow);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Glyph {
|
|
pub fn with_fade(&mut self, fade: Option<GlyphFade>) -> &mut Self {
|
|
self.fade = fade;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "scene_test.rs"]
|
|
mod tests;
|