StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use crate::elements::Fill; |
| 2 | use crate::geometry::vector::vec2f; |
| 3 | use crate::image_cache::StaticImage; |
| 4 | use crate::{ |
| 5 | elements::Point, |
| 6 | fonts::{FontId, GlyphId}, |
| 7 | rendering, |
| 8 | }; |
| 9 | use ordered_float::OrderedFloat; |
| 10 | use pathfinder_color::ColorU; |
| 11 | use pathfinder_geometry::rect::RectF; |
| 12 | use pathfinder_geometry::vector::Vector2F; |
| 13 | use rstar::{primitives::Rectangle, RTree}; |
| 14 | use std::sync::Arc; |
| 15 | use vec1::{vec1, Vec1}; |
| 16 | |
| 17 | #[derive(Clone)] |
| 18 | pub struct Scene { |
| 19 | scale_factor: f32, |
| 20 | rendering_config: rendering::Config, |
| 21 | active_layer_index_stack: Vec1<ZIndex>, |
| 22 | layers: Vec1<Layer>, |
| 23 | overlay_layers: Vec<Layer>, |
| 24 | #[cfg(debug_assertions)] |
| 25 | /// Custom panic location, set with [`Scene::set_location_for_panic_logging`] |
| 26 | panic_location: Option<&'static std::panic::Location<'static>>, |
| 27 | } |
| 28 | |
| 29 | #[derive(Clone, Default)] |
| 30 | pub struct Layer { |
| 31 | hit_map: RTree<Rectangle<[OrderedFloat<f32>; 2]>>, |
| 32 | pub clip_bounds: Option<RectF>, |
| 33 | pub rects: Vec<Rect>, |
| 34 | pub images: Vec<Image>, |
| 35 | pub glyphs: Vec<Glyph>, |
| 36 | pub icons: Vec<Icon>, |
| 37 | pub click_through: bool, |
| 38 | } |
| 39 | |
| 40 | /// Clip bounds to use for a layer. |
| 41 | pub enum ClipBounds { |
| 42 | /// Use the bounds of the active layer. |
| 43 | ActiveLayer, |
| 44 | /// Use the specified bounds as the bounds for the new layer. |
| 45 | /// |
| 46 | /// Note that this ignores any clip bounds applied to the currently-active |
| 47 | /// layer. |
| 48 | BoundedBy(RectF), |
| 49 | /// Intersect the active layer's bounds and the provided rect |
| 50 | /// to get the bounds for the new layer. |
| 51 | BoundedByActiveLayerAnd(RectF), |
| 52 | /// No clipping |
| 53 | None, |
| 54 | } |
| 55 | |
| 56 | impl Layer { |
| 57 | fn record_hit_rect(&mut self, rect: RectF) { |
| 58 | if let Some(intersected) = self |
| 59 | .clip_bounds |
| 60 | .map_or(Some(rect), |c| rect.intersection(c)) |
| 61 | { |
| 62 | self.hit_map.insert(Rectangle::from_corners( |
| 63 | [intersected.min_x().into(), intersected.min_y().into()], |
| 64 | [intersected.max_x().into(), intersected.max_y().into()], |
| 65 | )); |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] |
| 71 | pub struct GlyphKey { |
| 72 | pub glyph_id: GlyphId, |
| 73 | pub font_id: FontId, |
| 74 | pub font_size: OrderedFloat<f32>, |
| 75 | } |
| 76 | |
| 77 | #[derive(Debug, Copy, Clone)] |
| 78 | pub enum GlyphFade { |
| 79 | /// A horizontal fade from alpha 1 to 0 with start and end positions in screen coordinates |
| 80 | /// start - where the fade is transparent |
| 81 | /// end - where the fade is most opaque |
| 82 | Horizontal { start: f32, end: f32 }, |
| 83 | } |
| 84 | |
| 85 | impl GlyphFade { |
| 86 | pub fn horizontal(start: f32, end: f32) -> Self { |
| 87 | GlyphFade::Horizontal { start, end } |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | #[derive(Clone, Debug)] |
| 92 | pub struct Glyph { |
| 93 | pub glyph_key: GlyphKey, |
| 94 | pub position: Vector2F, |
| 95 | pub fade: Option<GlyphFade>, |
| 96 | pub color: ColorU, |
| 97 | } |
| 98 | |
| 99 | #[derive(Clone, Default)] |
| 100 | pub struct Rect { |
| 101 | pub bounds: RectF, |
| 102 | pub drop_shadow: Option<DropShadow>, |
| 103 | pub corner_radius: CornerRadius, |
| 104 | pub background: Fill, |
| 105 | pub border: Border, |
| 106 | } |
| 107 | |
| 108 | #[derive(Clone)] |
| 109 | pub struct Image { |
| 110 | pub bounds: RectF, |
| 111 | pub asset: Arc<StaticImage>, |
| 112 | pub opacity: f32, |
| 113 | pub corner_radius: CornerRadius, |
| 114 | } |
| 115 | |
| 116 | #[derive(Clone)] |
| 117 | pub struct Icon { |
| 118 | pub bounds: RectF, |
| 119 | pub asset: Arc<StaticImage>, |
| 120 | pub opacity: f32, |
| 121 | pub color: ColorU, |
| 122 | } |
| 123 | |
| 124 | // These were picked empirically to make the shadows look decent by |
| 125 | // default, but there is nothing special about them. |
| 126 | const DEFAULT_DROP_SHADOW_OFFSET_X: f32 = 0.; |
| 127 | const DEFAULT_DROP_SHADOW_OFFSET_Y: f32 = 10.; |
| 128 | const DEFAULT_DROP_SHADOW_BLUR_RADIUS: f32 = 10.; |
| 129 | const DEFAULT_DROP_SHADOW_SPREAD_RADIUS: f32 = 30.; |
| 130 | |
| 131 | #[derive(Clone, Copy)] |
| 132 | pub struct DropShadow { |
| 133 | pub color: ColorU, |
| 134 | |
| 135 | // How the shadow is offset from the target rect |
| 136 | pub offset: Vector2F, |
| 137 | |
| 138 | // Controls how tightly sampled the shadow is - the larger the number |
| 139 | // the more spread out the shadow. |
| 140 | pub blur_radius: f32, |
| 141 | |
| 142 | // Controls how wide the shadow is outside the target. |
| 143 | pub spread_radius: f32, |
| 144 | } |
| 145 | |
| 146 | impl DropShadow { |
| 147 | pub fn new_with_standard_offset_and_spread(color: ColorU) -> Self { |
| 148 | Self { |
| 149 | color, |
| 150 | offset: vec2f(DEFAULT_DROP_SHADOW_OFFSET_X, DEFAULT_DROP_SHADOW_OFFSET_Y), |
| 151 | blur_radius: DEFAULT_DROP_SHADOW_BLUR_RADIUS, |
| 152 | spread_radius: DEFAULT_DROP_SHADOW_SPREAD_RADIUS, |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | pub fn with_offset(mut self, offset: Vector2F) -> Self { |
| 157 | self.offset = offset; |
| 158 | self |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | impl Default for DropShadow { |
| 163 | fn default() -> Self { |
| 164 | Self::new_with_standard_offset_and_spread(ColorU::new(0, 0, 0, 32)) |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | #[derive(Debug, Clone, Copy, Default, PartialEq)] |
| 169 | pub struct Border { |
| 170 | pub width: f32, |
| 171 | pub color: Fill, |
| 172 | pub top: bool, |
| 173 | pub left: bool, |
| 174 | pub bottom: bool, |
| 175 | pub right: bool, |
| 176 | pub dash: Option<Dash>, |
| 177 | } |
| 178 | |
| 179 | #[derive(Debug, Clone, Copy, Default, PartialEq)] |
| 180 | pub struct Dash { |
| 181 | pub dash_length: f32, |
| 182 | pub gap_length: f32, |
| 183 | |
| 184 | /// If true, gaps will always be the length specified in `gap_length`. |
| 185 | /// Otherwise, gap length may be adjusted slightly to guarantee that the |
| 186 | /// dashed line starts and ends with a dash. |
| 187 | pub force_consistent_gap_length: bool, |
| 188 | } |
| 189 | |
| 190 | impl Border { |
| 191 | pub fn top_width(&self) -> f32 { |
| 192 | if self.top { |
| 193 | self.width |
| 194 | } else { |
| 195 | 0.0 |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | pub fn right_width(&self) -> f32 { |
| 200 | if self.right { |
| 201 | self.width |
| 202 | } else { |
| 203 | 0.0 |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | pub fn bottom_width(&self) -> f32 { |
| 208 | if self.bottom { |
| 209 | self.width |
| 210 | } else { |
| 211 | 0.0 |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | pub fn left_width(&self) -> f32 { |
| 216 | if self.left { |
| 217 | self.width |
| 218 | } else { |
| 219 | 0.0 |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 225 | pub enum Radius { |
| 226 | /// Specify a radius in absolute pixels. |
| 227 | Pixels(f32), |
| 228 | /// Specify a radius as a percentage of the rectangle's smaller dimension. |
| 229 | /// For example, using `Percentage(50.)` will produce a pill shape. |
| 230 | Percentage(f32), |
| 231 | } |
| 232 | |
| 233 | impl Default for Radius { |
| 234 | fn default() -> Self { |
| 235 | Radius::Pixels(0.) |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | #[derive(Clone, Copy, Debug, Default, PartialEq)] |
| 240 | pub struct CornerRadius { |
| 241 | /// Top left corner radius |
| 242 | top_left: Option<Radius>, |
| 243 | /// Top right corner radius |
| 244 | top_right: Option<Radius>, |
| 245 | /// Bottom left corner radius |
| 246 | bottom_left: Option<Radius>, |
| 247 | /// Bottom right corner radius |
| 248 | bottom_right: Option<Radius>, |
| 249 | } |
| 250 | |
| 251 | impl CornerRadius { |
| 252 | /// Merge this CornerRadius struct with another. |
| 253 | /// `Some(r)` takes precedence over `None`. |
| 254 | /// If both are present, `other`'s values, take precedence over `self`'s existing values. |
| 255 | pub fn merge(&mut self, other: CornerRadius) { |
| 256 | self.top_left = other.top_left.or(self.top_left); |
| 257 | self.top_right = other.top_right.or(self.top_right); |
| 258 | self.bottom_left = other.bottom_left.or(self.bottom_left); |
| 259 | self.bottom_right = other.bottom_right.or(self.bottom_right); |
| 260 | } |
| 261 | |
| 262 | pub fn get_top_left(&self) -> Radius { |
| 263 | self.top_left.unwrap_or(Radius::Pixels(0.)) |
| 264 | } |
| 265 | |
| 266 | pub fn get_top_right(&self) -> Radius { |
| 267 | self.top_right.unwrap_or(Radius::Pixels(0.)) |
| 268 | } |
| 269 | |
| 270 | pub fn get_bottom_left(&self) -> Radius { |
| 271 | self.bottom_left.unwrap_or(Radius::Pixels(0.)) |
| 272 | } |
| 273 | |
| 274 | pub fn get_bottom_right(&self) -> Radius { |
| 275 | self.bottom_right.unwrap_or(Radius::Pixels(0.)) |
| 276 | } |
| 277 | |
| 278 | pub const fn with_all(radius: Radius) -> Self { |
| 279 | CornerRadius { |
| 280 | top_left: Some(radius), |
| 281 | top_right: Some(radius), |
| 282 | bottom_left: Some(radius), |
| 283 | bottom_right: Some(radius), |
| 284 | } |
| 285 | } |
| 286 | pub const fn with_top(radius: Radius) -> Self { |
| 287 | CornerRadius { |
| 288 | top_left: Some(radius), |
| 289 | top_right: Some(radius), |
| 290 | bottom_left: None, |
| 291 | bottom_right: None, |
| 292 | } |
| 293 | } |
| 294 | pub const fn with_bottom(radius: Radius) -> Self { |
| 295 | CornerRadius { |
| 296 | top_left: None, |
| 297 | top_right: None, |
| 298 | bottom_left: Some(radius), |
| 299 | bottom_right: Some(radius), |
| 300 | } |
| 301 | } |
| 302 | pub const fn with_left(radius: Radius) -> Self { |
| 303 | CornerRadius { |
| 304 | top_left: Some(radius), |
| 305 | top_right: None, |
| 306 | bottom_left: Some(radius), |
| 307 | bottom_right: None, |
| 308 | } |
| 309 | } |
| 310 | pub const fn with_right(radius: Radius) -> Self { |
| 311 | CornerRadius { |
| 312 | top_left: None, |
| 313 | top_right: Some(radius), |
| 314 | bottom_left: None, |
| 315 | bottom_right: Some(radius), |
| 316 | } |
| 317 | } |
| 318 | pub const fn with_top_left(radius: Radius) -> Self { |
| 319 | CornerRadius { |
| 320 | top_left: Some(radius), |
| 321 | top_right: None, |
| 322 | bottom_left: None, |
| 323 | bottom_right: None, |
| 324 | } |
| 325 | } |
| 326 | pub const fn with_top_right(radius: Radius) -> Self { |
| 327 | CornerRadius { |
| 328 | top_left: None, |
| 329 | top_right: Some(radius), |
| 330 | bottom_left: None, |
| 331 | bottom_right: None, |
| 332 | } |
| 333 | } |
| 334 | pub const fn with_bottom_left(radius: Radius) -> Self { |
| 335 | CornerRadius { |
| 336 | top_left: None, |
| 337 | top_right: None, |
| 338 | bottom_left: Some(radius), |
| 339 | bottom_right: None, |
| 340 | } |
| 341 | } |
| 342 | pub const fn with_bottom_right(radius: Radius) -> Self { |
| 343 | CornerRadius { |
| 344 | top_left: None, |
| 345 | top_right: None, |
| 346 | bottom_left: None, |
| 347 | bottom_right: Some(radius), |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | /// Filters this [`CornerRadius`] to only have the top corners rounded. |
| 352 | pub const fn top(self) -> Self { |
| 353 | CornerRadius { |
| 354 | top_left: self.top_left, |
| 355 | top_right: self.top_right, |
| 356 | bottom_left: None, |
| 357 | bottom_right: None, |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | /// Filters this [`CornerRadius`] to only have the bottom corners rounded. |
| 362 | pub const fn bottom(self) -> Self { |
| 363 | CornerRadius { |
| 364 | top_left: None, |
| 365 | top_right: None, |
| 366 | bottom_left: self.bottom_left, |
| 367 | bottom_right: self.bottom_right, |
| 368 | } |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] |
| 373 | /// Newtype to encapsulate a Z index, which actually represents a layer index in the list of layers |
| 374 | pub enum ZIndex { |
| 375 | Normal(usize), |
| 376 | Overlay(usize), |
| 377 | } |
| 378 | |
| 379 | impl ZIndex { |
| 380 | #[cfg(test)] |
| 381 | pub fn new(layer: usize) -> Self { |
| 382 | ZIndex::Normal(layer) |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | impl Scene { |
| 387 | pub fn new(scale_factor: f32, rendering_config: rendering::Config) -> Self { |
| 388 | Self { |
| 389 | scale_factor, |
| 390 | rendering_config, |
| 391 | active_layer_index_stack: vec1![ZIndex::Normal(0)], |
| 392 | layers: vec1![Layer::default()], |
| 393 | overlay_layers: Vec::new(), |
| 394 | #[cfg(debug_assertions)] |
| 395 | panic_location: None, |
| 396 | } |
| 397 | } |
| 398 | |
| 399 | /// Temporarily set the panic location for the scene. This is cleared |
| 400 | /// during the next draw call. |
| 401 | #[cfg(debug_assertions)] |
| 402 | pub fn set_location_for_panic_logging( |
| 403 | &mut self, |
| 404 | panic_location: Option<&'static std::panic::Location<'static>>, |
| 405 | ) { |
| 406 | self.panic_location = panic_location; |
| 407 | } |
| 408 | |
| 409 | fn active_layer(&mut self) -> &mut Layer { |
| 410 | match *self.active_layer_index_stack.last() { |
| 411 | ZIndex::Normal(index) => &mut self.layers[index], |
| 412 | ZIndex::Overlay(index) => &mut self.overlay_layers[index], |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | pub fn is_covered(&self, position: Point) -> bool { |
| 417 | // Does any layer at a higher z-index contain this point? |
| 418 | let point = [position.x().into(), position.y().into()]; |
| 419 | let predicate = |l: &Layer| !l.click_through && l.hit_map.locate_at_point(&point).is_some(); |
| 420 | |
| 421 | match position.z_index() { |
| 422 | ZIndex::Normal(index) => self |
| 423 | .layers |
| 424 | .get((index + 1)..) |
| 425 | .into_iter() |
| 426 | .flatten() |
| 427 | .chain(self.overlay_layers.iter()) |
| 428 | .any(predicate), |
| 429 | ZIndex::Overlay(index) => self |
| 430 | .overlay_layers |
| 431 | .get((index + 1)..) |
| 432 | .into_iter() |
| 433 | .flatten() |
| 434 | .any(predicate), |
| 435 | } |
| 436 | } |
| 437 | |
| 438 | // Compute the intersection between the bound of the element and the clip bound |
| 439 | // on its current layer. The intersection is then checked against the event position |
| 440 | // to determine whether we should dispatch the event. |
| 441 | pub fn visible_rect(&self, origin: Point, size: Vector2F) -> Option<RectF> { |
| 442 | // TODO: Investigate how / when we would pass a z-index that isn't in the scene |
| 443 | // This appears to be fairly common, based on adding sentry reporting to it, however it |
| 444 | // doesn't seem to dramatically impact app usage. Perhaps it's something that happens on |
| 445 | // a view teardown frame? |
| 446 | let maybe_layer = match origin.z_index() { |
| 447 | ZIndex::Normal(index) => self.layers.get(index), |
| 448 | ZIndex::Overlay(index) => self.overlay_layers.get(index), |
| 449 | }; |
| 450 | let maybe_bounds = maybe_layer.and_then(|layer| layer.clip_bounds); |
| 451 | |
| 452 | let input_rect = RectF::new(origin.xy(), size); |
| 453 | match maybe_bounds { |
| 454 | Some(clip_rect) => clip_rect.intersection(input_rect), |
| 455 | None => Some(input_rect), |
| 456 | } |
| 457 | } |
| 458 | |
| 459 | /// Get the Z-Index of the currently-active layer |
| 460 | pub fn z_index(&self) -> ZIndex { |
| 461 | *self.active_layer_index_stack.last() |
| 462 | } |
| 463 | |
| 464 | /// Get the maximum Z-Index in the active layer stack (whether Normal or Overlay). |
| 465 | pub fn max_active_z_index(&self) -> ZIndex { |
| 466 | match self.active_layer_index_stack.last() { |
| 467 | ZIndex::Normal(_) => ZIndex::Normal(self.layers.len() - 1), |
| 468 | // Safety: If the active layer is an overlay layer, then there must be at least one |
| 469 | // overlay layer, so subtracting one from the length is valid. |
| 470 | ZIndex::Overlay(_) => ZIndex::Overlay(self.overlay_layers.len() - 1), |
| 471 | } |
| 472 | } |
| 473 | |
| 474 | pub fn start_layer(&mut self, bounds: ClipBounds) { |
| 475 | let layer = self.create_layer(bounds); |
| 476 | |
| 477 | match *self.active_layer_index_stack.last() { |
| 478 | ZIndex::Normal(_) => self.push_normal_layer(layer), |
| 479 | ZIndex::Overlay(_) => self.push_overlay_layer(layer), |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | pub(crate) fn start_overlay_layer(&mut self, bounds: ClipBounds) { |
| 484 | let layer = self.create_layer(bounds); |
| 485 | self.push_overlay_layer(layer); |
| 486 | } |
| 487 | |
| 488 | fn create_layer(&mut self, bounds: ClipBounds) -> Layer { |
| 489 | let clip_bounds = match bounds { |
| 490 | ClipBounds::ActiveLayer => self.active_layer().clip_bounds, |
| 491 | ClipBounds::BoundedBy(bounds) => Some(bounds), |
| 492 | ClipBounds::BoundedByActiveLayerAnd(bounds) => { |
| 493 | if let Some(current_layer_bounds) = self.active_layer().clip_bounds { |
| 494 | // If the current layer has bounds, return the intersection... |
| 495 | current_layer_bounds |
| 496 | .intersection(bounds) |
| 497 | // ...or, if the regions don't overlap, an empty bounding rect. |
| 498 | .or(Some(RectF::default())) |
| 499 | } else { |
| 500 | // If the current layer has no bounds, return the bounds |
| 501 | // for the new layer. |
| 502 | Some(bounds) |
| 503 | } |
| 504 | } |
| 505 | ClipBounds::None => None, |
| 506 | }; |
| 507 | |
| 508 | Layer { |
| 509 | clip_bounds, |
| 510 | ..Default::default() |
| 511 | } |
| 512 | } |
| 513 | |
| 514 | fn push_normal_layer(&mut self, layer: Layer) { |
| 515 | self.active_layer_index_stack |
| 516 | .push(ZIndex::Normal(self.layers.len())); |
| 517 | self.layers.push(layer); |
| 518 | } |
| 519 | |
| 520 | fn push_overlay_layer(&mut self, layer: Layer) { |
| 521 | self.active_layer_index_stack |
| 522 | .push(ZIndex::Overlay(self.overlay_layers.len())); |
| 523 | self.overlay_layers.push(layer); |
| 524 | } |
| 525 | |
| 526 | pub fn set_active_layer_click_through(&mut self) { |
| 527 | self.active_layer().click_through = true; |
| 528 | } |
| 529 | |
| 530 | pub fn stop_layer(&mut self) { |
| 531 | if self.active_layer_index_stack.pop().is_err() { |
| 532 | panic!("popped the last layer from active_layer_index_stack"); |
| 533 | } |
| 534 | } |
| 535 | |
| 536 | fn validate_rect(rect: &RectF, location: Option<&'static std::panic::Location<'static>>) { |
| 537 | #[cfg(debug_assertions)] |
| 538 | let location_info = location |
| 539 | .map(|loc| { |
| 540 | format!( |
| 541 | " (element created at {}:{}:{})", |
| 542 | loc.file(), |
| 543 | loc.line(), |
| 544 | loc.column() |
| 545 | ) |
| 546 | }) |
| 547 | .unwrap_or_default(); |
| 548 | #[cfg(not(debug_assertions))] |
| 549 | let location_info = ""; |
| 550 | debug_assert!( |
| 551 | !rect.origin().y().is_infinite(), |
| 552 | "!rect.origin().y().is_infinite(){location_info}" |
| 553 | ); |
| 554 | debug_assert!( |
| 555 | !rect.origin().y().is_nan(), |
| 556 | "!rect.origin().y().is_nan(){location_info}" |
| 557 | ); |
| 558 | |
| 559 | debug_assert!( |
| 560 | !rect.size().x().is_infinite(), |
| 561 | "!rect.size().x().is_infinite(){location_info}" |
| 562 | ); |
| 563 | debug_assert!( |
| 564 | !rect.size().x().is_nan(), |
| 565 | "!rect.size().x().is_nan(){location_info}" |
| 566 | ); |
| 567 | debug_assert!( |
| 568 | !rect.size().y().is_infinite(), |
| 569 | "!rect.size().y().is_infinite(){location_info}" |
| 570 | ); |
| 571 | debug_assert!( |
| 572 | !rect.size().y().is_nan(), |
| 573 | "!rect.size().y().is_nan(){location_info}" |
| 574 | ); |
| 575 | } |
| 576 | |
| 577 | /// This method draws a rectangle without recording any information about it in the current |
| 578 | /// layer. Note this should be used with caution. In most cases, what you need is |
| 579 | /// `draw_rect_with_hit_recording` instead. However, in rare cases this may be useful for |
| 580 | /// performance reasons when many intermediate rects are drawn. If this is called, it is up to |
| 581 | /// the caller to also draw a rect (via draw_rect_with_hit_recording) that encompasses the range |
| 582 | /// of the rects drawn so that layer recording for event dispatching is correctly kept |
| 583 | /// up-to-date. |
| 584 | pub fn draw_rect_without_hit_recording(&mut self, rect: RectF) -> &mut Rect { |
| 585 | #[cfg(debug_assertions)] |
| 586 | let location = self.panic_location.take(); |
| 587 | #[cfg(not(debug_assertions))] |
| 588 | let location = None; |
| 589 | let layer = self.active_layer(); |
| 590 | Self::validate_rect(&rect, location); |
| 591 | |
| 592 | layer.rects.push(Rect { |
| 593 | bounds: rect, |
| 594 | ..Default::default() |
| 595 | }); |
| 596 | layer.rects.last_mut().unwrap() |
| 597 | } |
| 598 | |
| 599 | pub fn draw_rect_with_hit_recording(&mut self, rect: RectF) -> &mut Rect { |
| 600 | let layer = self.active_layer(); |
| 601 | layer.record_hit_rect(rect); |
| 602 | self.draw_rect_without_hit_recording(rect) |
| 603 | } |
| 604 | |
| 605 | pub fn draw_image( |
| 606 | &mut self, |
| 607 | rect: RectF, |
| 608 | asset: Arc<StaticImage>, |
| 609 | opacity: f32, |
| 610 | corner_radius: CornerRadius, |
| 611 | ) { |
| 612 | #[cfg(debug_assertions)] |
| 613 | let location = self.panic_location.take(); |
| 614 | #[cfg(not(debug_assertions))] |
| 615 | let location = None; |
| 616 | let layer = self.active_layer(); |
| 617 | Self::validate_rect(&rect, location); |
| 618 | |
| 619 | layer.images.push(Image { |
| 620 | bounds: rect, |
| 621 | asset, |
| 622 | opacity, |
| 623 | corner_radius, |
| 624 | }); |
| 625 | layer.record_hit_rect(rect); |
| 626 | } |
| 627 | |
| 628 | pub fn draw_icon(&mut self, rect: RectF, asset: Arc<StaticImage>, opacity: f32, color: ColorU) { |
| 629 | #[cfg(debug_assertions)] |
| 630 | let location = self.panic_location.take(); |
| 631 | #[cfg(not(debug_assertions))] |
| 632 | let location = None; |
| 633 | let layer = self.active_layer(); |
| 634 | Self::validate_rect(&rect, location); |
| 635 | |
| 636 | layer.icons.push(Icon { |
| 637 | bounds: rect, |
| 638 | asset, |
| 639 | opacity, |
| 640 | color, |
| 641 | }); |
| 642 | layer.record_hit_rect(rect); |
| 643 | } |
| 644 | |
| 645 | /// Adds a glyph that should be drawn in the scene. |
| 646 | /// |
| 647 | /// `position` is the point at which the glyph's left edge meets the |
| 648 | /// baseline. |
| 649 | pub fn draw_glyph( |
| 650 | &mut self, |
| 651 | position: Vector2F, |
| 652 | glyph_id: GlyphId, |
| 653 | font_id: FontId, |
| 654 | font_size: f32, |
| 655 | color: ColorU, |
| 656 | ) -> &mut Glyph { |
| 657 | // TODO: Support hit testing on glyphs? |
| 658 | let layer = self.active_layer(); |
| 659 | layer.glyphs.push(Glyph { |
| 660 | glyph_key: GlyphKey { |
| 661 | glyph_id, |
| 662 | font_id, |
| 663 | font_size: font_size.into(), |
| 664 | }, |
| 665 | position, |
| 666 | color, |
| 667 | fade: None, |
| 668 | }); |
| 669 | layer.glyphs.last_mut().unwrap() |
| 670 | } |
| 671 | |
| 672 | /// Get an iterator over all layers in order, from bottom to top |
| 673 | pub fn layers(&self) -> impl Iterator<Item = &Layer> { |
| 674 | self.layers.iter().chain(self.overlay_layers.iter()) |
| 675 | } |
| 676 | |
| 677 | /// Get the total number of layers |
| 678 | #[cfg(test)] |
| 679 | pub fn layer_count(&self) -> usize { |
| 680 | self.layers.len() + self.overlay_layers.len() |
| 681 | } |
| 682 | |
| 683 | pub fn scale_factor(&self) -> f32 { |
| 684 | self.scale_factor |
| 685 | } |
| 686 | |
| 687 | pub fn rendering_config(&self) -> &rendering::Config { |
| 688 | &self.rendering_config |
| 689 | } |
| 690 | } |
| 691 | |
| 692 | impl Rect { |
| 693 | pub fn with_corner_radius(&mut self, radius: CornerRadius) -> &mut Self { |
| 694 | self.corner_radius.merge(radius); |
| 695 | self |
| 696 | } |
| 697 | |
| 698 | pub fn with_border(&mut self, border: Border) -> &mut Self { |
| 699 | self.border = border; |
| 700 | self |
| 701 | } |
| 702 | |
| 703 | pub fn with_background<F>(&mut self, background: F) -> &mut Self |
| 704 | where |
| 705 | F: Into<Fill>, |
| 706 | { |
| 707 | self.background = background.into(); |
| 708 | self |
| 709 | } |
| 710 | |
| 711 | pub fn with_drop_shadow(&mut self, drop_shadow: DropShadow) -> &mut Self { |
| 712 | self.drop_shadow = Some(drop_shadow); |
| 713 | self |
| 714 | } |
| 715 | } |
| 716 | |
| 717 | impl Glyph { |
| 718 | pub fn with_fade(&mut self, fade: Option<GlyphFade>) -> &mut Self { |
| 719 | self.fade = fade; |
| 720 | self |
| 721 | } |
| 722 | } |
| 723 | |
| 724 | #[cfg(test)] |
| 725 | #[path = "scene_test.rs"] |
| 726 | mod tests; |
| 727 |