Add retained paints and shared text selection
This commit is contained in:
1 parent
1e6d3b1edd
commit
a33fbca966
42 files changed
+2424
-470
No files matched your search
@@ -0,0 +1,261 @@
|
||||
use crate::{
|
||||
ActiveData, WidgetId,
|
||||
util::{HashMap, HashSet},
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct ControllerId {
|
||||
host: WidgetId,
|
||||
kind: TypeId,
|
||||
}
|
||||
|
||||
impl ControllerId {
|
||||
pub fn host(self) -> WidgetId {
|
||||
self.host
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Command {
|
||||
Copy,
|
||||
SelectAll,
|
||||
Escape,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum CommandResult {
|
||||
Unused,
|
||||
Used,
|
||||
Copy(String),
|
||||
}
|
||||
|
||||
pub trait ControllerValue: Any {
|
||||
fn into_any(self: Box<Self>) -> Box<dyn Any>;
|
||||
}
|
||||
|
||||
impl<T: Any> ControllerValue for T {
|
||||
fn into_any(self: Box<Self>) -> Box<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Controller<Rsc>: ControllerValue {
|
||||
fn command(&mut self, _command: Command, _rsc: &mut Rsc) -> CommandResult {
|
||||
CommandResult::Unused
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ControllerManager<Rsc> {
|
||||
by_widget: HashMap<WidgetId, HashMap<TypeId, Box<dyn Controller<Rsc>>>>,
|
||||
parents: HashMap<WidgetId, Option<WidgetId>>,
|
||||
borrowed: HashSet<ControllerId>,
|
||||
removed_while_borrowed: HashSet<WidgetId>,
|
||||
command_target: Option<ControllerId>,
|
||||
command_target_revision: u64,
|
||||
command_boundary: Option<WidgetId>,
|
||||
}
|
||||
|
||||
impl<Rsc> Default for ControllerManager<Rsc> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
by_widget: Default::default(),
|
||||
parents: Default::default(),
|
||||
borrowed: Default::default(),
|
||||
removed_while_borrowed: Default::default(),
|
||||
command_target: None,
|
||||
command_target_revision: 0,
|
||||
command_boundary: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: 'static> ControllerManager<Rsc> {
|
||||
#[track_caller]
|
||||
pub fn register<C: Controller<Rsc>>(&mut self, host: WidgetId, controller: C) {
|
||||
let kind = TypeId::of::<C>();
|
||||
let id = ControllerId { host, kind };
|
||||
assert!(
|
||||
!self.borrowed.contains(&id),
|
||||
"a controller cannot be replaced while it is handling input"
|
||||
);
|
||||
assert!(
|
||||
!self.removed_while_borrowed.contains(&host),
|
||||
"a controller cannot be attached to a removed widget"
|
||||
);
|
||||
let old = self
|
||||
.by_widget
|
||||
.entry(host)
|
||||
.or_default()
|
||||
.insert(kind, Box::new(controller));
|
||||
assert!(
|
||||
old.is_none(),
|
||||
"a widget cannot have two controllers of type {}",
|
||||
std::any::type_name::<C>()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn id<C: Controller<Rsc>>(&self, host: WidgetId) -> Option<ControllerId> {
|
||||
let kind = TypeId::of::<C>();
|
||||
self.by_widget
|
||||
.get(&host)?
|
||||
.contains_key(&kind)
|
||||
.then_some(ControllerId { host, kind })
|
||||
}
|
||||
|
||||
pub fn nearest_id<C: Controller<Rsc>>(&self, mut origin: WidgetId) -> Option<ControllerId> {
|
||||
let kind = TypeId::of::<C>();
|
||||
loop {
|
||||
let candidate = ControllerId { host: origin, kind };
|
||||
assert!(
|
||||
!self.borrowed.contains(&candidate),
|
||||
"a controller cannot re-enter itself while it is handling input"
|
||||
);
|
||||
if let Some(id) = self.id::<C>(origin) {
|
||||
return Some(id);
|
||||
}
|
||||
origin = self.parents.get(&origin).copied().flatten()?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_to<C: Controller<Rsc>>(
|
||||
&self,
|
||||
mut origin: WidgetId,
|
||||
) -> Option<(ControllerId, Vec<WidgetId>)> {
|
||||
let mut path = Vec::new();
|
||||
loop {
|
||||
path.push(origin);
|
||||
if let Some(id) = self.id::<C>(origin) {
|
||||
return Some((id, path));
|
||||
}
|
||||
origin = self.parents.get(&origin).copied().flatten()?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, active: &ActiveData) {
|
||||
self.parents.insert(active.id, active.parent);
|
||||
}
|
||||
|
||||
pub fn undraw(&mut self, active: &ActiveData) {
|
||||
self.parents.remove(&active.id);
|
||||
}
|
||||
|
||||
pub fn take<C: Controller<Rsc>>(&mut self, id: ControllerId) -> Option<C> {
|
||||
if id.kind != TypeId::of::<C>() {
|
||||
return None;
|
||||
}
|
||||
assert!(
|
||||
!self.borrowed.contains(&id),
|
||||
"a controller cannot re-enter itself while it is handling input"
|
||||
);
|
||||
let boxed = self.by_widget.get_mut(&id.host)?.remove(&id.kind)?;
|
||||
self.borrowed.insert(id);
|
||||
let boxed = boxed.into_any();
|
||||
boxed.downcast().ok().map(|boxed| *boxed)
|
||||
}
|
||||
|
||||
pub fn put<C: Controller<Rsc>>(&mut self, id: ControllerId, controller: C) {
|
||||
debug_assert_eq!(id.kind, TypeId::of::<C>());
|
||||
assert!(
|
||||
self.borrowed.remove(&id),
|
||||
"restored an unborrowed controller"
|
||||
);
|
||||
if self.finish_removed_host(id.host) {
|
||||
return;
|
||||
}
|
||||
let old = self
|
||||
.by_widget
|
||||
.entry(id.host)
|
||||
.or_default()
|
||||
.insert(id.kind, Box::new(controller));
|
||||
debug_assert!(old.is_none(), "a controller was re-entered while borrowed");
|
||||
}
|
||||
|
||||
fn take_dyn(&mut self, id: ControllerId) -> Option<Box<dyn Controller<Rsc>>> {
|
||||
assert!(
|
||||
!self.borrowed.contains(&id),
|
||||
"a controller cannot re-enter itself while it is handling input"
|
||||
);
|
||||
let controller = self.by_widget.get_mut(&id.host)?.remove(&id.kind)?;
|
||||
self.borrowed.insert(id);
|
||||
Some(controller)
|
||||
}
|
||||
|
||||
fn put_dyn(&mut self, id: ControllerId, controller: Box<dyn Controller<Rsc>>) {
|
||||
assert!(
|
||||
self.borrowed.remove(&id),
|
||||
"restored an unborrowed controller"
|
||||
);
|
||||
if self.finish_removed_host(id.host) {
|
||||
return;
|
||||
}
|
||||
let old = self
|
||||
.by_widget
|
||||
.entry(id.host)
|
||||
.or_default()
|
||||
.insert(id.kind, controller);
|
||||
debug_assert!(old.is_none(), "a controller was re-entered while borrowed");
|
||||
}
|
||||
|
||||
pub fn set_command_target(&mut self, target: Option<ControllerId>) {
|
||||
self.command_target = target;
|
||||
self.command_target_revision = self.command_target_revision.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn command_target(&self) -> Option<ControllerId> {
|
||||
self.command_target
|
||||
}
|
||||
|
||||
pub(crate) fn command_target_revision(&self) -> u64 {
|
||||
self.command_target_revision
|
||||
}
|
||||
|
||||
pub(crate) fn command_boundary(&self) -> Option<WidgetId> {
|
||||
self.command_boundary
|
||||
}
|
||||
|
||||
pub(crate) fn set_command_boundary(&mut self, boundary: Option<WidgetId>) {
|
||||
self.command_boundary = boundary;
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, host: WidgetId) {
|
||||
self.by_widget.remove(&host);
|
||||
self.parents.remove(&host);
|
||||
if self.borrowed.iter().any(|id| id.host == host) {
|
||||
self.removed_while_borrowed.insert(host);
|
||||
}
|
||||
if self.command_target.is_some_and(|id| id.host == host) {
|
||||
self.command_target = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn take_command_target(
|
||||
&mut self,
|
||||
) -> Option<(ControllerId, Box<dyn Controller<Rsc>>)> {
|
||||
let id = self.command_target?;
|
||||
match self.take_dyn(id) {
|
||||
Some(controller) => Some((id, controller)),
|
||||
None => {
|
||||
self.command_target = None;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn restore(&mut self, id: ControllerId, controller: Box<dyn Controller<Rsc>>) {
|
||||
self.put_dyn(id, controller);
|
||||
}
|
||||
|
||||
/// Returns true when a host disappeared during its controller callback,
|
||||
/// in which case restoring the temporarily extracted value would revive
|
||||
/// state belonging to a dead widget generation.
|
||||
fn finish_removed_host(&mut self, host: WidgetId) -> bool {
|
||||
if !self.removed_while_borrowed.contains(&host) {
|
||||
return false;
|
||||
}
|
||||
if !self.borrowed.iter().any(|id| id.host == host) {
|
||||
self.removed_while_borrowed.remove(&host);
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId,
|
||||
WeakWidget, WidgetEventFn, WidgetId,
|
||||
ActiveData, ControllerManager, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents,
|
||||
IdLike, LayerId, WeakWidget, WidgetEventFn, WidgetId,
|
||||
util::{HashMap, HashSet, TypeMap},
|
||||
};
|
||||
use std::{any::TypeId, rc::Rc};
|
||||
@@ -8,13 +8,15 @@ use std::{any::TypeId, rc::Rc};
|
||||
pub struct EventManager<Rsc> {
|
||||
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
|
||||
types: TypeMap<dyn EventManagerLike<Rsc>>,
|
||||
pub controllers: ControllerManager<Rsc>,
|
||||
}
|
||||
|
||||
impl<Rsc> Default for EventManager<Rsc> {
|
||||
impl<Rsc: 'static> Default for EventManager<Rsc> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
widget_to_types: Default::default(),
|
||||
types: Default::default(),
|
||||
controllers: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,15 +56,18 @@ impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
|
||||
for t in self.widget_to_types.get(&id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().remove(id);
|
||||
}
|
||||
self.controllers.remove(id);
|
||||
}
|
||||
|
||||
fn draw(&mut self, active: &ActiveData) {
|
||||
self.controllers.draw(active);
|
||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().draw(active);
|
||||
}
|
||||
}
|
||||
|
||||
fn undraw(&mut self, active: &ActiveData) {
|
||||
self.controllers.undraw(active);
|
||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().undraw(active);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
mod controller;
|
||||
mod ctx;
|
||||
mod manager;
|
||||
mod rsc;
|
||||
|
||||
pub use controller::*;
|
||||
pub use ctx::*;
|
||||
pub use manager::*;
|
||||
pub use rsc::*;
|
||||
|
||||
+71
-1
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
||||
Command, CommandResult, Controller, ControllerId, Event, EventCtx, EventLike, EventManager,
|
||||
IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
||||
};
|
||||
|
||||
pub trait HasState: 'static {
|
||||
@@ -18,6 +19,75 @@ pub trait HasEvents: Sized + UiRsc + HasState {
|
||||
) {
|
||||
self.events_mut().register(id, event, f);
|
||||
}
|
||||
|
||||
fn register_controller<W: ?Sized, C: Controller<Self>>(
|
||||
&mut self,
|
||||
id: WeakWidget<W>,
|
||||
controller: C,
|
||||
) {
|
||||
self.events_mut().controllers.register(id.id(), controller);
|
||||
}
|
||||
|
||||
fn with_controller<C: Controller<Self>, T>(
|
||||
&mut self,
|
||||
id: ControllerId,
|
||||
f: impl FnOnce(&mut C, &mut Self) -> T,
|
||||
) -> Option<T> {
|
||||
let mut controller = self.events_mut().controllers.take::<C>(id)?;
|
||||
let result = f(&mut controller, self);
|
||||
self.events_mut().controllers.put(id, controller);
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn with_nearest_controller<C: Controller<Self>, T>(
|
||||
&mut self,
|
||||
origin: impl IdLike,
|
||||
f: impl FnOnce(ControllerId, &mut C, &mut Self) -> T,
|
||||
) -> Option<T> {
|
||||
let id = self.events().controllers.nearest_id::<C>(origin.id())?;
|
||||
self.with_controller(id, |controller, rsc| f(id, controller, rsc))
|
||||
}
|
||||
|
||||
fn set_command_target(&mut self, target: Option<ControllerId>) {
|
||||
self.events_mut().controllers.set_command_target(target);
|
||||
}
|
||||
|
||||
fn run_command(&mut self, command: Command) -> CommandResult {
|
||||
let revision = self.events().controllers.command_target_revision();
|
||||
if self
|
||||
.events()
|
||||
.controllers
|
||||
.command_target()
|
||||
.is_some_and(|target| {
|
||||
self.events().controllers.command_boundary() == Some(target.host())
|
||||
})
|
||||
{
|
||||
return CommandResult::Unused;
|
||||
}
|
||||
let Some((id, mut controller)) = self.events_mut().controllers.take_command_target() else {
|
||||
return CommandResult::Unused;
|
||||
};
|
||||
let result = controller.command(command, self);
|
||||
self.events_mut().controllers.restore(id, controller);
|
||||
if command == Command::Escape
|
||||
&& result != CommandResult::Unused
|
||||
&& self.events().controllers.command_target_revision() == revision
|
||||
{
|
||||
self.set_command_target(None);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn run_command_before(&mut self, command: Command, boundary: impl IdLike) -> CommandResult {
|
||||
let old = self.events().controllers.command_boundary();
|
||||
self.events_mut()
|
||||
.controllers
|
||||
.set_command_boundary(Some(boundary.id()));
|
||||
let result = self.run_command(command);
|
||||
self.events_mut().controllers.set_command_boundary(old);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RunEvents: HasEvents {
|
||||
|
||||
@@ -30,5 +30,3 @@ pub use primitive::*;
|
||||
pub use render::*;
|
||||
pub use ui::*;
|
||||
pub use widget::*;
|
||||
|
||||
pub type UiColor = primitive::Color<u8>;
|
||||
+489
-142
@@ -1,171 +1,518 @@
|
||||
use std::marker::Destruct;
|
||||
use crate::util::{Dirty, RefCounter};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fmt,
|
||||
rc::Rc,
|
||||
sync::mpsc::{Receiver, Sender, channel},
|
||||
};
|
||||
|
||||
/// Encoded, straight-alpha sRGB at an input boundary.
|
||||
///
|
||||
/// Palette literals, decoded images and colour glyph bitmaps use this
|
||||
/// convention. A solid paint is converted to linear light when it enters the
|
||||
/// paint table; the renderer never performs colour arithmetic on these bytes.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, bytemuck::Zeroable, Debug)]
|
||||
pub struct Color<T> {
|
||||
pub r: T,
|
||||
pub g: T,
|
||||
pub b: T,
|
||||
pub a: T,
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Srgba8 {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
/// Required by parley's `Brush`, which every text style is generic over. Opaque
|
||||
/// black rather than transparent: a brush that was never set should be visible
|
||||
/// and obviously unstyled, not invisible.
|
||||
impl<T: ColorNum> Default for Color<T> {
|
||||
impl Srgba8 {
|
||||
pub const BLACK: Self = Self::rgb(0, 0, 0);
|
||||
pub const WHITE: Self = Self::rgb(255, 255, 255);
|
||||
pub const GRAY: Self = Self::rgb(127, 127, 127);
|
||||
pub const RED: Self = Self::rgb(255, 0, 0);
|
||||
pub const ORANGE: Self = Self::rgb(255, 127, 0);
|
||||
pub const YELLOW: Self = Self::rgb(255, 255, 0);
|
||||
pub const LIME: Self = Self::rgb(127, 255, 0);
|
||||
pub const GREEN: Self = Self::rgb(0, 255, 0);
|
||||
pub const TURQUOISE: Self = Self::rgb(0, 255, 127);
|
||||
pub const CYAN: Self = Self::rgb(0, 255, 255);
|
||||
pub const SKY: Self = Self::rgb(0, 127, 255);
|
||||
pub const BLUE: Self = Self::rgb(0, 0, 255);
|
||||
pub const PURPLE: Self = Self::rgb(127, 0, 255);
|
||||
pub const MAGENTA: Self = Self::rgb(255, 0, 255);
|
||||
pub const NONE: Self = Self::new(0, 0, 0, 0);
|
||||
|
||||
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self::new(r, g, b, 255)
|
||||
}
|
||||
|
||||
pub fn to_linear(self) -> LinearRgba {
|
||||
LinearRgba::new(
|
||||
srgb_to_linear(self.r as f32 / 255.0),
|
||||
srgb_to_linear(self.g as f32 / 255.0),
|
||||
srgb_to_linear(self.b as f32 / 255.0),
|
||||
self.a as f32 / 255.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Straight-alpha RGBA in linear-light sRGB primaries.
|
||||
///
|
||||
/// This is Iris's working representation: manipulate and interpolate colours
|
||||
/// here, then put the result in [`Paints`]. The GPU paint buffer stores this
|
||||
/// exact layout as `vec4<f32>`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct LinearRgba {
|
||||
pub r: f32,
|
||||
pub g: f32,
|
||||
pub b: f32,
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl LinearRgba {
|
||||
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
|
||||
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
|
||||
pub const NONE: Self = Self::new(0.0, 0.0, 0.0, 0.0);
|
||||
|
||||
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
|
||||
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
|
||||
Self::new(r, g, b, 1.0)
|
||||
}
|
||||
|
||||
pub fn mul_rgb(self, amount: f32) -> Self {
|
||||
Self::new(self.r * amount, self.g * amount, self.b * amount, self.a)
|
||||
}
|
||||
|
||||
pub fn darker(self, amount: f32) -> Self {
|
||||
self.mul_rgb(1.0 - amount)
|
||||
}
|
||||
|
||||
pub fn brighter(self, amount: f32) -> Self {
|
||||
Self::new(
|
||||
self.r + (1.0 - self.r) * amount,
|
||||
self.g + (1.0 - self.g) * amount,
|
||||
self.b + (1.0 - self.b) * amount,
|
||||
self.a,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_wgpu(self) -> wgpu::Color {
|
||||
wgpu::Color {
|
||||
r: self.r as f64,
|
||||
g: self.g as f64,
|
||||
b: self.b as f64,
|
||||
a: self.a as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn srgb_to_linear(value: f32) -> f32 {
|
||||
if value <= 0.04045 {
|
||||
value / 12.92
|
||||
} else {
|
||||
((value + 0.055) / 1.055).powf(2.4)
|
||||
}
|
||||
}
|
||||
|
||||
/// A description that can be registered in Iris's paint table.
|
||||
///
|
||||
/// Only solid paints exist today. Keeping registration behind this trait and
|
||||
/// making primitives carry [`PaintId`] leaves one place to add gradient or
|
||||
/// texture paint records later.
|
||||
pub trait Paint: private::Sealed + 'static {
|
||||
#[doc(hidden)]
|
||||
fn add_to(&self, paints: &mut Paints) -> PaintId;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn replace(&self, paints: &mut Paints, slot: u32);
|
||||
|
||||
/// Erases this definition so a widget can register it lazily on its
|
||||
/// first draw. [`PaintId`] overrides this to stay a direct handle.
|
||||
#[doc(hidden)]
|
||||
fn into_value(self) -> PaintValue
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
PaintValue::pending(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Paint for Srgba8 {
|
||||
fn add_to(&self, paints: &mut Paints) -> PaintId {
|
||||
paints.add_linear(self.to_linear())
|
||||
}
|
||||
|
||||
fn replace(&self, paints: &mut Paints, slot: u32) {
|
||||
paints.replace_linear(slot, self.to_linear());
|
||||
}
|
||||
}
|
||||
|
||||
impl Paint for LinearRgba {
|
||||
fn add_to(&self, paints: &mut Paints) -> PaintId {
|
||||
paints.add_linear(*self)
|
||||
}
|
||||
|
||||
fn replace(&self, paints: &mut Paints, slot: u32) {
|
||||
paints.replace_linear(slot, *self);
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
impl Sealed for super::Srgba8 {}
|
||||
impl Sealed for super::LinearRgba {}
|
||||
impl Sealed for super::PaintId {}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PaintLease {
|
||||
slot: u32,
|
||||
counter: RefCounter,
|
||||
send: Sender<u32>,
|
||||
}
|
||||
|
||||
impl Clone for PaintLease {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
slot: self.slot,
|
||||
counter: self.counter.clone(),
|
||||
send: self.send.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PaintLease {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
let _ = self.send.send(self.slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stable reference to one entry in a UI's paint table.
|
||||
///
|
||||
/// Built-in IDs name the same reserved entries in every [`Paints`]. IDs
|
||||
/// returned by [`Paints::add`] retain their slot until the last clone held by a
|
||||
/// widget, shaped text or retained draw is dropped.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PaintId {
|
||||
slot: u32,
|
||||
lease: Option<PaintLease>,
|
||||
}
|
||||
|
||||
impl PaintId {
|
||||
pub const BLACK: Self = Self::builtin(0);
|
||||
pub const WHITE: Self = Self::builtin(1);
|
||||
pub const GRAY: Self = Self::builtin(2);
|
||||
pub const RED: Self = Self::builtin(3);
|
||||
pub const ORANGE: Self = Self::builtin(4);
|
||||
pub const YELLOW: Self = Self::builtin(5);
|
||||
pub const LIME: Self = Self::builtin(6);
|
||||
pub const GREEN: Self = Self::builtin(7);
|
||||
pub const TURQUOISE: Self = Self::builtin(8);
|
||||
pub const CYAN: Self = Self::builtin(9);
|
||||
pub const SKY: Self = Self::builtin(10);
|
||||
pub const BLUE: Self = Self::builtin(11);
|
||||
pub const PURPLE: Self = Self::builtin(12);
|
||||
pub const MAGENTA: Self = Self::builtin(13);
|
||||
pub const NONE: Self = Self::builtin(14);
|
||||
|
||||
const fn builtin(slot: u32) -> Self {
|
||||
Self { slot, lease: None }
|
||||
}
|
||||
|
||||
pub(crate) fn slot(&self) -> u32 {
|
||||
self.slot
|
||||
}
|
||||
|
||||
fn is_managed(&self) -> bool {
|
||||
self.lease.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaintId {
|
||||
fn default() -> Self {
|
||||
Self::BLACK
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Color<T> {
|
||||
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
|
||||
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
|
||||
pub const GRAY: Self = Self::rgb(T::MID, T::MID, T::MID);
|
||||
|
||||
pub const RED: Self = Self::rgb(T::MAX, T::MIN, T::MIN);
|
||||
pub const ORANGE: Self = Self::rgb(T::MAX, T::MID, T::MIN);
|
||||
pub const YELLOW: Self = Self::rgb(T::MAX, T::MAX, T::MIN);
|
||||
pub const LIME: Self = Self::rgb(T::MID, T::MAX, T::MIN);
|
||||
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN);
|
||||
pub const TURQUOISE: Self = Self::rgb(T::MIN, T::MAX, T::MID);
|
||||
pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
|
||||
pub const SKY: Self = Self::rgb(T::MIN, T::MID, T::MAX);
|
||||
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX);
|
||||
pub const PURPLE: Self = Self::rgb(T::MID, T::MIN, T::MAX);
|
||||
pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
|
||||
|
||||
pub const NONE: Self = Self::new(T::MIN, T::MIN, T::MIN, T::MIN);
|
||||
|
||||
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
pub const fn rgb(r: T, g: T, b: T) -> Self {
|
||||
Self { r, g, b, a: T::MAX }
|
||||
}
|
||||
pub fn alpha(mut self, a: T) -> Self {
|
||||
self.a = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_arr(self) -> [T; 4] {
|
||||
[self.r, self.g, self.b, self.a]
|
||||
impl PartialEq for PaintId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.slot == other.slot
|
||||
}
|
||||
}
|
||||
|
||||
pub const trait F32Conversion {
|
||||
fn to(self) -> f32;
|
||||
fn from(x: f32) -> Self;
|
||||
impl Eq for PaintId {}
|
||||
|
||||
impl Paint for PaintId {
|
||||
fn add_to(&self, _paints: &mut Paints) -> PaintId {
|
||||
self.clone()
|
||||
}
|
||||
|
||||
fn replace(&self, paints: &mut Paints, slot: u32) {
|
||||
let value = paints.entries[self.slot as usize];
|
||||
paints.replace_linear(slot, value);
|
||||
}
|
||||
|
||||
fn into_value(self) -> PaintValue {
|
||||
PaintValue(PaintValueInner::Id(self))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ColorNum {
|
||||
const MIN: Self;
|
||||
const MID: Self;
|
||||
const MAX: Self;
|
||||
struct PendingPaint {
|
||||
definition: Box<dyn Paint>,
|
||||
resolved: RefCell<Option<PaintId>>,
|
||||
}
|
||||
|
||||
macro_rules! map_rgb {
|
||||
($x:ident,$self:ident, $e:tt) => {
|
||||
#[allow(unused_braces)]
|
||||
Self {
|
||||
r: {
|
||||
let $x = $self.r;
|
||||
$e
|
||||
},
|
||||
g: {
|
||||
let $x = $self.g;
|
||||
$e
|
||||
},
|
||||
b: {
|
||||
let $x = $self.b;
|
||||
$e
|
||||
},
|
||||
a: $self.a,
|
||||
#[derive(Clone)]
|
||||
enum PaintValueInner {
|
||||
Id(PaintId),
|
||||
Pending(Rc<PendingPaint>),
|
||||
}
|
||||
|
||||
/// A widget property containing either an existing paint-table ID or a paint
|
||||
/// definition that will receive an ID the first time it is drawn.
|
||||
///
|
||||
/// Pending definitions are shared across clones and registered only once.
|
||||
/// Each property replaces its own pending variant with the resulting direct
|
||||
/// ID after that first resolution, so later draws take the direct path.
|
||||
#[derive(Clone)]
|
||||
pub struct PaintValue(PaintValueInner);
|
||||
|
||||
impl PaintValue {
|
||||
fn pending(paint: impl Paint) -> Self {
|
||||
Self(PaintValueInner::Pending(Rc::new(PendingPaint {
|
||||
definition: Box::new(paint),
|
||||
resolved: RefCell::new(None),
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn resolve(&mut self, paints: &mut Paints) -> &PaintId {
|
||||
if let PaintValueInner::Pending(pending) = &self.0 {
|
||||
let resolved = pending.resolved.borrow().clone();
|
||||
let id = match resolved {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let id = pending.definition.add_to(paints);
|
||||
*pending.resolved.borrow_mut() = Some(id.clone());
|
||||
id
|
||||
}
|
||||
};
|
||||
self.0 = PaintValueInner::Id(id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<T: ColorNum + const F32Conversion> Color<T>
|
||||
where
|
||||
Self: const Destruct,
|
||||
{
|
||||
pub const fn mul_rgb(self, amt: impl const F32Conversion) -> Self {
|
||||
let amt = amt.to();
|
||||
map_rgb!(x, self, { T::from(x.to() * amt) })
|
||||
let PaintValueInner::Id(id) = &self.0 else {
|
||||
unreachable!()
|
||||
};
|
||||
id
|
||||
}
|
||||
|
||||
pub const fn add_rgb(self, amt: impl const F32Conversion) -> Self {
|
||||
let amt = amt.to();
|
||||
map_rgb!(x, self, { T::from(x.to() + amt) })
|
||||
}
|
||||
|
||||
pub const fn darker(self, amt: f32) -> Self {
|
||||
self.mul_rgb(1.0 - amt)
|
||||
}
|
||||
|
||||
pub const fn brighter(self, amt: f32) -> Self {
|
||||
map_rgb!(x, self, {
|
||||
let x = x.to();
|
||||
T::from(x + (1.0 - x) * amt)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_rgb(self, f: impl Fn(T) -> T) -> Self {
|
||||
Self {
|
||||
r: f(self.r),
|
||||
g: f(self.g),
|
||||
b: f(self.b),
|
||||
a: self.a,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn srgb(r: T, g: T, b: T) -> Self {
|
||||
Self {
|
||||
r: s_to_l(r),
|
||||
g: s_to_l(g),
|
||||
b: s_to_l(b),
|
||||
a: T::MAX,
|
||||
pub fn is(&self, id: &PaintId) -> bool {
|
||||
match &self.0 {
|
||||
PaintValueInner::Id(current) => current == id,
|
||||
PaintValueInner::Pending(pending) => pending.resolved.borrow().as_ref() == Some(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn s_to_l<T: F32Conversion>(x: T) -> T {
|
||||
let x = x.to();
|
||||
T::from(if x <= 0.0405 {
|
||||
x / 12.92
|
||||
} else {
|
||||
((x + 0.055) / 1.055).powf(2.4)
|
||||
})
|
||||
}
|
||||
|
||||
impl ColorNum for u8 {
|
||||
const MIN: Self = u8::MIN;
|
||||
const MID: Self = u8::MAX / 2;
|
||||
const MAX: Self = u8::MAX;
|
||||
}
|
||||
|
||||
impl ColorNum for f32 {
|
||||
const MIN: Self = 0.0;
|
||||
const MID: Self = 0.5;
|
||||
const MAX: Self = 1.0;
|
||||
}
|
||||
|
||||
unsafe impl bytemuck::Pod for Color<u8> {}
|
||||
|
||||
const impl F32Conversion for f32 {
|
||||
fn to(self) -> f32 {
|
||||
self
|
||||
}
|
||||
fn from(x: f32) -> Self {
|
||||
x
|
||||
impl fmt::Debug for PaintValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.0 {
|
||||
PaintValueInner::Id(id) => f.debug_tuple("PaintValue").field(id).finish(),
|
||||
PaintValueInner::Pending(_) => f.write_str("PaintValue(Pending)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const impl F32Conversion for u8 {
|
||||
fn to(self) -> f32 {
|
||||
self as f32 / 255.0
|
||||
const BUILTIN_PAINTS: [Srgba8; 15] = [
|
||||
Srgba8::BLACK,
|
||||
Srgba8::WHITE,
|
||||
Srgba8::GRAY,
|
||||
Srgba8::RED,
|
||||
Srgba8::ORANGE,
|
||||
Srgba8::YELLOW,
|
||||
Srgba8::LIME,
|
||||
Srgba8::GREEN,
|
||||
Srgba8::TURQUOISE,
|
||||
Srgba8::CYAN,
|
||||
Srgba8::SKY,
|
||||
Srgba8::BLUE,
|
||||
Srgba8::PURPLE,
|
||||
Srgba8::MAGENTA,
|
||||
Srgba8::NONE,
|
||||
];
|
||||
|
||||
/// CPU-side paint table and the dirty set for its GPU mirror.
|
||||
pub struct Paints {
|
||||
entries: Vec<LinearRgba>,
|
||||
free: Vec<u32>,
|
||||
dirty: Dirty,
|
||||
send: Sender<u32>,
|
||||
recv: Receiver<u32>,
|
||||
}
|
||||
|
||||
impl Paints {
|
||||
pub fn new() -> Self {
|
||||
let (send, recv) = channel();
|
||||
Self {
|
||||
entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(),
|
||||
free: Vec::new(),
|
||||
dirty: Dirty::new_all(),
|
||||
send,
|
||||
recv,
|
||||
}
|
||||
}
|
||||
fn from(x: f32) -> Self {
|
||||
(x * 255.0).clamp(0.0, 255.0) as Self
|
||||
|
||||
pub fn add(&mut self, paint: impl Paint) -> PaintId {
|
||||
paint.add_to(self)
|
||||
}
|
||||
|
||||
fn add_linear(&mut self, value: LinearRgba) -> PaintId {
|
||||
self.free_released();
|
||||
let slot = if let Some(slot) = self.free.pop() {
|
||||
self.entries[slot as usize] = value;
|
||||
self.dirty.mark(slot as usize);
|
||||
slot
|
||||
} else {
|
||||
let slot = self.entries.len() as u32;
|
||||
self.entries.push(value);
|
||||
self.dirty.mark(slot as usize);
|
||||
slot
|
||||
};
|
||||
PaintId {
|
||||
slot,
|
||||
lease: Some(PaintLease {
|
||||
slot,
|
||||
counter: RefCounter::new(),
|
||||
send: self.send.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces one managed paint in place. Every primitive keeps the same
|
||||
/// index, so a theme change dirties this table and no primitive buffer.
|
||||
pub fn set(&mut self, id: &PaintId, paint: impl Paint) {
|
||||
assert!(
|
||||
id.is_managed(),
|
||||
"a reserved built-in paint cannot be replaced; allocate a theme slot with Paints::add"
|
||||
);
|
||||
paint.replace(self, id.slot);
|
||||
}
|
||||
|
||||
fn replace_linear(&mut self, slot: u32, value: LinearRgba) {
|
||||
self.entries[slot as usize] = value;
|
||||
self.dirty.mark(slot as usize);
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &PaintId) -> LinearRgba {
|
||||
self.entries[id.slot as usize]
|
||||
}
|
||||
|
||||
pub fn free_released(&mut self) {
|
||||
for slot in self.recv.try_iter() {
|
||||
self.entries[slot as usize] = LinearRgba::NONE;
|
||||
self.dirty.mark(slot as usize);
|
||||
self.free.push(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/// A new GPU device has no copy of this table even when the CPU-side UI
|
||||
/// and its paint IDs survived an Android surface recreation.
|
||||
pub fn reupload(&mut self) {
|
||||
self.dirty.mark_all();
|
||||
}
|
||||
|
||||
pub fn for_upload(&mut self) -> (&[LinearRgba], &mut Dirty) {
|
||||
(&self.entries, &mut self.dirty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Paints {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn srgb_bytes_become_linear_without_transforming_alpha() {
|
||||
let got = Srgba8::new(17, 127, 255, 64).to_linear();
|
||||
assert!((got.r - 0.005605).abs() < 0.000001);
|
||||
assert!((got.g - 0.212231).abs() < 0.000001);
|
||||
assert_eq!(got.b, 1.0);
|
||||
assert!((got.a - 64.0 / 255.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_a_paint_keeps_its_id_and_dirties_only_its_slot() {
|
||||
let mut paints = Paints::new();
|
||||
let id = paints.add(Srgba8::rgb(17, 17, 27));
|
||||
let slot = id.slot();
|
||||
let (_, dirty) = paints.for_upload();
|
||||
dirty.clear();
|
||||
|
||||
paints.set(&id, Srgba8::rgb(205, 214, 244));
|
||||
let (_, dirty) = paints.for_upload();
|
||||
assert!(dirty.contains(slot as usize));
|
||||
assert_eq!(id.slot(), slot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_released_paint_slot_is_reused_only_after_the_last_clone() {
|
||||
let mut paints = Paints::new();
|
||||
let id = paints.add(Srgba8::RED);
|
||||
let slot = id.slot();
|
||||
let held = id.clone();
|
||||
drop(id);
|
||||
paints.free_released();
|
||||
let other = paints.add(Srgba8::GREEN);
|
||||
assert_ne!(other.slot(), slot);
|
||||
|
||||
drop(held);
|
||||
paints.free_released();
|
||||
let reused = paints.add(Srgba8::BLUE);
|
||||
assert_eq!(reused.slot(), slot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_pending_paints_register_once_and_then_become_direct_ids() {
|
||||
let mut paints = Paints::new();
|
||||
let mut first = Srgba8::rgb(17, 17, 27).into_value();
|
||||
let mut second = first.clone();
|
||||
let before = paints.entries.len();
|
||||
|
||||
let first_id = first.resolve(&mut paints).clone();
|
||||
let second_id = second.resolve(&mut paints).clone();
|
||||
|
||||
assert_eq!(first_id, second_id);
|
||||
assert_eq!(paints.entries.len(), before + 1);
|
||||
assert!(first.is(&first_id));
|
||||
assert!(second.is(&first_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independently_constructed_inline_solids_get_independent_slots() {
|
||||
let mut paints = Paints::new();
|
||||
let mut first = Srgba8::rgb(23, 42, 71).into_value();
|
||||
let mut second = Srgba8::rgb(23, 42, 71).into_value();
|
||||
let before = paints.entries.len();
|
||||
|
||||
let first_id = first.resolve(&mut paints).clone();
|
||||
let second_id = second.resolve(&mut paints).clone();
|
||||
assert_ne!(first_id, second_id);
|
||||
assert_eq!(paints.entries.len(), before + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_theme_paints_with_equal_values_remain_independent() {
|
||||
let mut paints = Paints::new();
|
||||
let first = paints.add(Srgba8::rgb(23, 42, 71));
|
||||
let second = paints.add(Srgba8::rgb(23, 42, 71));
|
||||
assert_ne!(first.slot(), second.slot());
|
||||
}
|
||||
}
|
||||
+29
-18
@@ -1,4 +1,4 @@
|
||||
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2};
|
||||
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2};
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
|
||||
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
@@ -32,7 +32,7 @@ pub struct FontDiagnostics {
|
||||
|
||||
pub struct TextData {
|
||||
pub font_cx: FontContext,
|
||||
pub layout_cx: LayoutContext<UiColor>,
|
||||
pub layout_cx: LayoutContext<PaintId>,
|
||||
scale_cx: ScaleContext,
|
||||
pub atlas: GlyphAtlas,
|
||||
/// Physical pixels per dp -- a second copy of
|
||||
@@ -240,8 +240,13 @@ impl TextData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
||||
pub fn place(
|
||||
&mut self,
|
||||
buffer: &TextBuffer,
|
||||
textures: &mut Textures,
|
||||
) -> (Vec<PlacedGlyph>, Vec<PaintId>) {
|
||||
let mut placed = Vec::new();
|
||||
let mut paints = Vec::new();
|
||||
for line in buffer.layout.lines() {
|
||||
for item in line.items() {
|
||||
let PositionedLayoutItem::GlyphRun(run) = item else {
|
||||
@@ -250,7 +255,10 @@ impl TextData {
|
||||
let font = run.run().font();
|
||||
let font_size = run.run().font_size();
|
||||
let coords = run.run().normalized_coords();
|
||||
let run_color = run.style().brush;
|
||||
let run_color = run.style().brush.clone();
|
||||
if !paints.contains(&run_color) {
|
||||
paints.push(run_color.clone());
|
||||
}
|
||||
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
||||
else {
|
||||
continue;
|
||||
@@ -301,12 +309,12 @@ impl TextData {
|
||||
glyph.x.floor() + entry.left as f32,
|
||||
glyph.y.floor() - entry.top as f32,
|
||||
),
|
||||
color: run_color,
|
||||
paint: run_color.slot(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
placed
|
||||
(placed, paints)
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
@@ -318,11 +326,12 @@ impl TextData {
|
||||
density: f32,
|
||||
) -> RenderedText {
|
||||
buffer.shape(self, attrs, width, density);
|
||||
let glyphs = self.place(buffer, textures);
|
||||
let (glyphs, paints) = self.place(buffer, textures);
|
||||
RenderedText {
|
||||
glyphs: std::sync::Arc::new(glyphs),
|
||||
paints: std::sync::Arc::new(paints),
|
||||
size: buffer.size(),
|
||||
color: attrs.color,
|
||||
color: attrs.color.clone(),
|
||||
generation: self.atlas.generation(),
|
||||
}
|
||||
}
|
||||
@@ -359,7 +368,7 @@ impl Family {
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SpanStyle {
|
||||
pub range: Range<usize>,
|
||||
pub color: Option<UiColor>,
|
||||
pub color: Option<PaintId>,
|
||||
pub family: Option<Family>,
|
||||
pub font_size: Option<f32>,
|
||||
pub bold: bool,
|
||||
@@ -379,7 +388,7 @@ impl SpanStyle {
|
||||
underline: false,
|
||||
}
|
||||
}
|
||||
pub fn color(mut self, color: UiColor) -> Self {
|
||||
pub fn color(mut self, color: PaintId) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
@@ -407,7 +416,7 @@ impl SpanStyle {
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TextAttrs {
|
||||
pub color: UiColor,
|
||||
pub color: PaintId,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: Family,
|
||||
@@ -421,7 +430,7 @@ impl Default for TextAttrs {
|
||||
fn default() -> Self {
|
||||
let size = 16.0;
|
||||
Self {
|
||||
color: UiColor::WHITE,
|
||||
color: PaintId::WHITE,
|
||||
font_size: size,
|
||||
line_height: size * LINE_HEIGHT_MULT,
|
||||
family: Family::SansSerif,
|
||||
@@ -436,7 +445,7 @@ impl Default for TextAttrs {
|
||||
/// them apart is how they get out of step.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
layout: Layout<UiColor>,
|
||||
layout: Layout<PaintId>,
|
||||
spans: Vec<SpanStyle>,
|
||||
shaped: Option<(TextAttrs, Option<f32>, f32)>,
|
||||
}
|
||||
@@ -464,7 +473,7 @@ impl TextBuffer {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> &Layout<UiColor> {
|
||||
pub fn layout(&self) -> &Layout<PaintId> {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
@@ -515,11 +524,11 @@ impl TextBuffer {
|
||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||
attrs.line_height * density,
|
||||
)));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color.clone()));
|
||||
for (span, family) in self.spans.iter().zip(&span_families) {
|
||||
let range = span.range.clone();
|
||||
if let Some(color) = span.color {
|
||||
builder.push(StyleProperty::Brush(color), range.clone());
|
||||
if let Some(color) = &span.color {
|
||||
builder.push(StyleProperty::Brush(color.clone()), range.clone());
|
||||
}
|
||||
if let Some(family) = family {
|
||||
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
||||
@@ -563,8 +572,10 @@ fn hash_coords(coords: &[i16]) -> u64 {
|
||||
#[derive(Clone)]
|
||||
pub struct RenderedText {
|
||||
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
||||
/// The unique handles whose compact slots the glyphs above carry.
|
||||
pub paints: std::sync::Arc<Vec<PaintId>>,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
pub color: PaintId,
|
||||
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
|
||||
/// A holder must re-render rather than re-emit these quads once the
|
||||
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
PatchRect, TextureHandle, Textures, UiColor,
|
||||
PatchRect, TextureHandle, Textures,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use image::RgbaImage;
|
||||
@@ -202,5 +202,5 @@ fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
||||
pub struct PlacedGlyph {
|
||||
pub entry: GlyphEntry,
|
||||
pub offset: Vec2,
|
||||
pub color: UiColor,
|
||||
pub paint: u32,
|
||||
}
|
||||
+78
-5
@@ -30,6 +30,46 @@ pub use sdf::{distance_from_rect, rounded_rect_coverage};
|
||||
|
||||
pub const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
/// The advertised swapchain format and the sRGB view Iris renders through.
|
||||
/// A backend may advertise only the non-sRGB member of an RGBA/BGRA pair;
|
||||
/// wgpu permits its sRGB counterpart as a configured view format.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SurfaceFormat {
|
||||
pub surface: TextureFormat,
|
||||
pub view: TextureFormat,
|
||||
}
|
||||
|
||||
pub fn srgb_surface_format(caps: &SurfaceCapabilities) -> Result<SurfaceFormat, String> {
|
||||
let supports_srgb_space = |format| caps.color_spaces(format).contains(SurfaceColorSpaces::SRGB);
|
||||
if let Some(surface) = caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|format| format.is_srgb() && supports_srgb_space(*format))
|
||||
{
|
||||
return Ok(SurfaceFormat {
|
||||
surface,
|
||||
view: surface,
|
||||
});
|
||||
}
|
||||
if let Some(surface) = caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|format| format.add_srgb_suffix().is_srgb() && supports_srgb_space(*format))
|
||||
{
|
||||
return Ok(SurfaceFormat {
|
||||
surface,
|
||||
view: surface.add_srgb_suffix(),
|
||||
});
|
||||
}
|
||||
Err(format!(
|
||||
"the surface has no RGBA/BGRA format with an sRGB render view and sRGB output colour \
|
||||
space; advertised default formats: {:?}",
|
||||
caps.formats
|
||||
))
|
||||
}
|
||||
|
||||
pub fn device_limits() -> Limits {
|
||||
Limits {
|
||||
max_buffer_size: 1 << 30,
|
||||
@@ -91,6 +131,7 @@ pub struct UiRenderNode {
|
||||
instances: ArrBuf<PrimitiveInstance>,
|
||||
masks: ArrBuf<Mask>,
|
||||
move_offsets: ArrBuf<MoveOffset>,
|
||||
paints: ArrBuf<crate::LinearRgba>,
|
||||
masks_layout: BindGroupLayout,
|
||||
masks_group: BindGroup,
|
||||
}
|
||||
@@ -196,13 +237,16 @@ impl UiRenderNode {
|
||||
let masks_resized = self.masks.update(device, queue, entries, dirty);
|
||||
let (entries, dirty) = ui.move_offsets.for_upload();
|
||||
let moves_resized = self.move_offsets.update(device, queue, entries, dirty);
|
||||
if masks_resized || moves_resized || instances_resized {
|
||||
let (entries, dirty) = ui.paints.for_upload();
|
||||
let paints_resized = self.paints.update(device, queue, entries, dirty);
|
||||
if masks_resized || moves_resized || instances_resized || paints_resized {
|
||||
self.masks_group = Self::masks_group(
|
||||
device,
|
||||
&self.masks_layout,
|
||||
&self.masks,
|
||||
&self.move_offsets,
|
||||
&self.instances,
|
||||
&self.paints,
|
||||
);
|
||||
}
|
||||
let rebuild_main = self.textures.update(&mut ui.textures, &self.rsc_layout);
|
||||
@@ -212,6 +256,7 @@ impl UiRenderNode {
|
||||
FrameUpdateStats {
|
||||
masks_resized,
|
||||
moves_resized,
|
||||
paints_resized,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +276,7 @@ impl UiRenderNode {
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
config: &SurfaceConfiguration,
|
||||
target_format: TextureFormat,
|
||||
window_size: impl Into<Vec2>,
|
||||
) -> Result<Self, String> {
|
||||
let oom_scope = device.push_error_scope(ErrorFilter::OutOfMemory);
|
||||
@@ -305,12 +350,23 @@ impl UiRenderNode {
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui move offsets",
|
||||
);
|
||||
let paints = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui paints",
|
||||
);
|
||||
|
||||
let rsc_layout = Self::rsc_layout(device);
|
||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
|
||||
let masks_layout = Self::masks_layout(device);
|
||||
let masks_group =
|
||||
Self::masks_group(device, &masks_layout, &masks, &move_offsets, &instances);
|
||||
let masks_group = Self::masks_group(
|
||||
device,
|
||||
&masks_layout,
|
||||
&masks,
|
||||
&move_offsets,
|
||||
&instances,
|
||||
&paints,
|
||||
);
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
@@ -335,7 +391,7 @@ impl UiRenderNode {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(ColorTargetState {
|
||||
format: config.format,
|
||||
format: target_format,
|
||||
blend: Some(BlendState::ALPHA_BLENDING),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
@@ -386,6 +442,7 @@ impl UiRenderNode {
|
||||
instances,
|
||||
masks,
|
||||
move_offsets,
|
||||
paints,
|
||||
masks_layout,
|
||||
masks_group,
|
||||
})
|
||||
@@ -522,6 +579,16 @@ impl UiRenderNode {
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui masks"),
|
||||
})
|
||||
@@ -533,6 +600,7 @@ impl UiRenderNode {
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
instances: &ArrBuf<PrimitiveInstance>,
|
||||
paints: &ArrBuf<crate::LinearRgba>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
@@ -549,6 +617,10 @@ impl UiRenderNode {
|
||||
binding: 2,
|
||||
resource: instances.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: paints.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui masks"),
|
||||
})
|
||||
@@ -576,4 +648,5 @@ impl UiRenderNode {
|
||||
pub struct FrameUpdateStats {
|
||||
pub masks_resized: bool,
|
||||
pub moves_resized: bool,
|
||||
pub paints_resized: bool,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{
|
||||
Color, UiRegion, WidgetId,
|
||||
UiRegion, WidgetId,
|
||||
render::{
|
||||
ArrBuf,
|
||||
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
||||
@@ -554,16 +554,18 @@ primitives!(
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RectPrimitive {
|
||||
pub color: Color<u8>,
|
||||
/// Index into the separate paint buffer. Geometry stays untouched when a
|
||||
/// theme replaces the value at this index.
|
||||
pub paint: u32,
|
||||
pub radius: f32,
|
||||
pub thickness: f32,
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
impl RectPrimitive {
|
||||
pub fn color(color: Color<u8>) -> Self {
|
||||
pub fn color(paint: u32) -> Self {
|
||||
Self {
|
||||
color,
|
||||
paint,
|
||||
radius: 0.0,
|
||||
thickness: 0.0,
|
||||
inner_radius: 0.0,
|
||||
@@ -580,7 +582,7 @@ pub struct GlyphPrimitive {
|
||||
/// not a bind-group or view index, since a page never gets one of its
|
||||
/// own. See TEXTURES.md's "Recommended shape".
|
||||
pub layer: u32,
|
||||
pub color: Color<u8>,
|
||||
pub paint: u32,
|
||||
pub flags: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
@@ -588,18 +590,12 @@ pub struct GlyphPrimitive {
|
||||
impl GlyphPrimitive {
|
||||
pub const IS_COLOR: u32 = 1;
|
||||
|
||||
pub fn new(
|
||||
uv_min: [f32; 2],
|
||||
uv_max: [f32; 2],
|
||||
layer: u32,
|
||||
color: Color<u8>,
|
||||
flags: u32,
|
||||
) -> Self {
|
||||
pub fn new(uv_min: [f32; 2], uv_max: [f32; 2], layer: u32, paint: u32, flags: u32) -> Self {
|
||||
Self {
|
||||
uv_min,
|
||||
uv_max,
|
||||
layer,
|
||||
color,
|
||||
paint,
|
||||
flags,
|
||||
_pad: 0,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ var<storage> rects: array<Rect>;
|
||||
var<storage> glyphs: array<GlyphInfo>;
|
||||
|
||||
struct Rect {
|
||||
color: u32,
|
||||
paint: u32,
|
||||
radius: f32,
|
||||
thickness: f32,
|
||||
inner_radius: f32,
|
||||
@@ -22,7 +22,7 @@ struct GlyphInfo {
|
||||
uv_max: vec2<f32>,
|
||||
// A layer in the shared atlas array, not a bind-group index.
|
||||
layer: u32,
|
||||
color: u32,
|
||||
paint: u32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ var<storage> move_offsets: array<MoveOffset>;
|
||||
// Shared by the vertex stage's drawn primitive and the fragment stage's mask shape.
|
||||
@group(3) @binding(2)
|
||||
var<storage> instances: array<PrimitiveInstance>;
|
||||
// Solid linear RGBA today. Primitives already refer to paint records rather
|
||||
// than embedding colours so gradients and texture fills can extend this
|
||||
// lookup without rewriting geometry.
|
||||
@group(3) @binding(3)
|
||||
var<storage> paints: array<vec4<f32>>;
|
||||
|
||||
// Keep synchronized with render_state.rs. The bound prevents a malformed
|
||||
// parent cycle from hanging the GPU; real widget trees have exceeded 16.
|
||||
@@ -222,7 +227,7 @@ fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
||||
if (g.flags & 1u) != 0u {
|
||||
return texel;
|
||||
}
|
||||
var color = unpack4x8unorm(g.color);
|
||||
var color = paints[g.paint];
|
||||
color.a *= texel.a;
|
||||
return color;
|
||||
}
|
||||
@@ -242,7 +247,7 @@ fn rounded_rect_coverage(
|
||||
}
|
||||
|
||||
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
||||
var color = unpack4x8unorm(rect.color);
|
||||
var color = paints[rect.paint];
|
||||
|
||||
let edge = 0.5;
|
||||
|
||||
|
||||
@@ -271,7 +271,11 @@ impl GpuTextures {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
// `image` and swash colour-glyph bytes are encoded sRGB.
|
||||
// Sampling this view decodes RGB to the linear-light values
|
||||
// used by the paint buffer and render pipeline; alpha stays
|
||||
// linear.
|
||||
format: TextureFormat::Rgba8UnormSrgb,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
},
|
||||
@@ -337,7 +341,10 @@ impl GpuTextures {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
// One array contains both alpha-mask glyphs and colour glyphs.
|
||||
// sRGB decoding leaves mask pages' white RGB and alpha unchanged
|
||||
// while correctly decoding colour-glyph RGB.
|
||||
format: TextureFormat::Rgba8UnormSrgb,
|
||||
usage: TextureUsages::TEXTURE_BINDING
|
||||
| TextureUsages::COPY_DST
|
||||
| TextureUsages::COPY_SRC,
|
||||
@@ -430,7 +437,7 @@ pub fn null_texture_view(device: &Device) -> TextureView {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
format: TextureFormat::Rgba8UnormSrgb,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, util::Vec2,
|
||||
LayerId, MaskIdx, MoveIdx, PaintId, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -8,6 +9,10 @@ pub struct ActiveData {
|
||||
pub region: UiRegion,
|
||||
pub parent: Option<WidgetId>,
|
||||
pub textures: Vec<TextureHandle>,
|
||||
/// Paint slots retained by this draw. The GPU primitive stores only the
|
||||
/// slot index, so these handles are what prevent a live primitive from
|
||||
/// observing a recycled paint.
|
||||
pub paints: Vec<PaintId>,
|
||||
pub primitives: Vec<PrimitiveHandle>,
|
||||
pub children: Vec<WidgetId>,
|
||||
pub size_dependencies: Vec<WidgetId>,
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
Mask, MoveOffset, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
};
|
||||
|
||||
mod access;
|
||||
@@ -15,6 +15,7 @@ pub use render_state::*;
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
pub widgets: Widgets,
|
||||
pub paints: Paints,
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
@@ -70,5 +71,6 @@ pub trait UiRsc {
|
||||
self.on_remove(id);
|
||||
}
|
||||
self.ui_mut().textures.free();
|
||||
self.ui_mut().paints.free_released();
|
||||
}
|
||||
}
|
||||
+22
-3
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
Axis, Color, Len, MoveOffset, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
|
||||
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
|
||||
TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
||||
WidgetId,
|
||||
render::{
|
||||
@@ -20,6 +20,7 @@ pub struct Painter<'a> {
|
||||
pub(super) child_move_slot: Option<MoveIdx>,
|
||||
pub(super) own_mask: MaskIdx,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) paints: Vec<PaintId>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
@@ -118,6 +119,20 @@ impl<'a> Painter<'a> {
|
||||
self.primitive_at(primitive, self.region)
|
||||
}
|
||||
|
||||
/// Resolves a public paint handle to the compact index stored by a GPU
|
||||
/// primitive and retains the handle for exactly as long as that draw.
|
||||
pub fn paint(&mut self, paint: &PaintId) -> u32 {
|
||||
if !self.paints.contains(paint) {
|
||||
self.paints.push(paint.clone());
|
||||
}
|
||||
paint.slot()
|
||||
}
|
||||
|
||||
pub fn paint_value(&mut self, paint: &mut crate::PaintValue) -> u32 {
|
||||
let paint = paint.resolve(&mut self.rsc.ui_mut().paints).clone();
|
||||
self.paint(&paint)
|
||||
}
|
||||
|
||||
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
self.primitive_at(primitive, region.within(&self.region));
|
||||
}
|
||||
@@ -128,7 +143,8 @@ impl<'a> Painter<'a> {
|
||||
/// so keeps pointing at whichever slot it was drawn under. See
|
||||
/// `ActiveData::own_mask` for what pushing a fresh one cost.
|
||||
pub fn set_mask(&mut self, region: UiRegion) {
|
||||
let shape = self.write_primitive(RectPrimitive::color(Color::NONE), region, Drawn::No);
|
||||
let paint = self.paint(&PaintId::NONE);
|
||||
let shape = self.write_primitive(RectPrimitive::color(paint), region, Drawn::No);
|
||||
self.set_mask_to(shape);
|
||||
}
|
||||
|
||||
@@ -433,6 +449,9 @@ impl<'a> Painter<'a> {
|
||||
0
|
||||
}
|
||||
};
|
||||
for paint in text.paints.iter() {
|
||||
self.paint(paint);
|
||||
}
|
||||
for glyph in text.glyphs.iter() {
|
||||
let mut region = origin;
|
||||
region.x.end = region.x.start;
|
||||
@@ -445,7 +464,7 @@ impl<'a> Painter<'a> {
|
||||
glyph.entry.uv_min,
|
||||
glyph.entry.uv_max,
|
||||
glyph.entry.layer,
|
||||
glyph.color,
|
||||
glyph.paint,
|
||||
flags_for(glyph.entry.is_color),
|
||||
),
|
||||
region,
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::{
|
||||
ActiveData, Axis, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, RegionAlign,
|
||||
Size, StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||
ActiveData, Axis, ChildOrder, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers,
|
||||
RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiVec2, Widget, WidgetId, Widgets,
|
||||
render::{
|
||||
Drawn, MoveOffset, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, Primitives,
|
||||
RectPrimitive, rounded_rect_coverage,
|
||||
@@ -73,6 +73,7 @@ pub(crate) struct Retained {
|
||||
pub child_move_slot: Option<MoveIdx>,
|
||||
pub own_mask: MaskIdx,
|
||||
pub primitives: Vec<PrimitiveHandle>,
|
||||
pub paints: Vec<crate::PaintId>,
|
||||
}
|
||||
|
||||
impl Default for Retained {
|
||||
@@ -84,6 +85,7 @@ impl Default for Retained {
|
||||
child_move_slot: None,
|
||||
own_mask: MaskIdx::NONE,
|
||||
primitives: Vec::new(),
|
||||
paints: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,6 +352,7 @@ impl UiRenderState {
|
||||
mut child_move_slot,
|
||||
mut own_mask,
|
||||
primitives: mut recycle,
|
||||
paints: _old_paints,
|
||||
} = retained;
|
||||
let dirty = rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
let requires_exact_region = rsc
|
||||
@@ -447,6 +450,7 @@ impl UiRenderState {
|
||||
layer,
|
||||
id,
|
||||
textures: Vec::new(),
|
||||
paints: Vec::new(),
|
||||
primitives: Vec::new(),
|
||||
recycle: recycle.into_iter().peekable(),
|
||||
children: Vec::new(),
|
||||
@@ -498,6 +502,7 @@ impl UiRenderState {
|
||||
child_move_slot,
|
||||
own_mask,
|
||||
textures,
|
||||
paints,
|
||||
primitives,
|
||||
recycle,
|
||||
children,
|
||||
@@ -517,6 +522,7 @@ impl UiRenderState {
|
||||
region,
|
||||
parent,
|
||||
textures,
|
||||
paints,
|
||||
primitives,
|
||||
children,
|
||||
size_dependencies,
|
||||
@@ -1046,6 +1052,41 @@ impl UiRenderState {
|
||||
Some(region.to_px(self.output_size))
|
||||
}
|
||||
|
||||
/// This widget's immediate rendered children in the order non-visual
|
||||
/// consumers should traverse them. The ordinary case costs no sort and
|
||||
/// exactly preserves draw order; axis-aware layout widgets ask for their
|
||||
/// resolved screen positions only when somebody actually queries them.
|
||||
pub fn ordered_children(&self, id: WidgetId, rsc: &dyn UiRsc) -> Vec<WidgetId> {
|
||||
let Some(active) = self.active.get(&id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut children = active.children.clone();
|
||||
// A layout may draw a child to learn its size and then place that
|
||||
// retained drawing. Both operations are recorded for redraw lifetime,
|
||||
// but a semantic traversal visits the child once.
|
||||
let mut seen = HashSet::default();
|
||||
children.retain(|child| seen.insert(*child));
|
||||
let order = rsc
|
||||
.widgets()
|
||||
.get_dyn(id)
|
||||
.map(Widget::child_order)
|
||||
.unwrap_or_default();
|
||||
if let ChildOrder::Axis(axis) = order {
|
||||
children.sort_by(|a, b| {
|
||||
let at = self
|
||||
.window_region(a, rsc)
|
||||
.map(|r| r.top_left.axis(axis))
|
||||
.unwrap_or(f32::INFINITY);
|
||||
let bt = self
|
||||
.window_region(b, rsc)
|
||||
.map(|r| r.top_left.axis(axis))
|
||||
.unwrap_or(f32::INFINITY);
|
||||
at.total_cmp(&bt)
|
||||
});
|
||||
}
|
||||
children
|
||||
}
|
||||
|
||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
if self.draw_started.contains(&id) {
|
||||
@@ -1075,6 +1116,7 @@ impl UiRenderState {
|
||||
child_move_slot: active.child_move_slot,
|
||||
own_mask: active.own_mask,
|
||||
primitives: active.primitives,
|
||||
paints: active.paints,
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
|
||||
@@ -15,6 +15,16 @@ pub use tag::*;
|
||||
pub use view::*;
|
||||
pub use widgets::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum ChildOrder {
|
||||
/// The order in which the parent drew its children.
|
||||
#[default]
|
||||
Draw,
|
||||
/// Ascending visual position on one screen axis. Equal positions keep
|
||||
/// draw order; the resolved coordinates are sorted only when queried.
|
||||
Axis(Axis),
|
||||
}
|
||||
|
||||
pub trait Widget: Any {
|
||||
fn draw(&mut self, painter: &mut Painter);
|
||||
|
||||
@@ -34,6 +44,10 @@ pub trait Widget: Any {
|
||||
accesskit::Role::Unknown
|
||||
}
|
||||
|
||||
fn child_order(&self) -> ChildOrder {
|
||||
ChildOrder::Draw
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn tick(&mut self, now: std::time::Instant) -> bool {
|
||||
false
|
||||
|
||||
Reference in new issue
Block a user