use std::collections::HashMap;
use bevy::prelude::*;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BrushTarget {
Background,
Foreground,
Fill,
Stroke,
}
impl BrushTarget {
#[must_use]
pub fn property(self) -> &'static str {
match self {
Self::Background => "Background",
Self::Foreground => "Foreground",
Self::Fill => "Fill",
Self::Stroke => "Stroke",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GradientStop {
pub offset: f32,
pub color: [f32; 4],
}
impl GradientStop {
#[must_use]
pub fn new(offset: f32, color: [f32; 4]) -> Self {
Self { offset, color }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum BrushSpec {
Solid([f32; 4]),
LinearGradient {
start: [f32; 2],
end: [f32; 2],
stops: Vec<GradientStop>,
},
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisBrushes {
pub brushes: HashMap<(String, BrushTarget), BrushSpec>,
}
impl NoesisBrushes {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn solid(mut self, name: impl Into<String>, target: BrushTarget, rgba: [f32; 4]) -> Self {
self.brushes
.insert((name.into(), target), BrushSpec::Solid(rgba));
self
}
#[must_use]
pub fn linear_gradient(
mut self,
name: impl Into<String>,
target: BrushTarget,
start: [f32; 2],
end: [f32; 2],
stops: Vec<GradientStop>,
) -> Self {
self.brushes.insert(
(name.into(), target),
BrushSpec::LinearGradient { start, end, stops },
);
self
}
pub fn paint_solid(&mut self, name: impl Into<String>, target: BrushTarget, rgba: [f32; 4]) {
self.brushes
.insert((name.into(), target), BrushSpec::Solid(rgba));
}
pub fn paint_linear_gradient(
&mut self,
name: impl Into<String>,
target: BrushTarget,
start: [f32; 2],
end: [f32; 2],
stops: Vec<GradientStop>,
) {
self.brushes.insert(
(name.into(), target),
BrushSpec::LinearGradient { start, end, stops },
);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BrushReadback {
Solid([f32; 4]),
NonSolid,
}
#[derive(Message, Debug, Clone)]
pub struct NoesisBrushChanged {
pub view: Entity,
pub name: String,
pub target: BrushTarget,
pub readback: BrushReadback,
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_brushes_bridge(
views: Query<(Entity, Ref<NoesisBrushes>)>,
state: Option<NonSendMut<NoesisRenderState>>,
mut changed: MessageWriter<NoesisBrushChanged>,
) {
let Some(mut state) = state else {
return;
};
for (entity, brushes) in &views {
if brushes.is_changed() || state.scene_rebuilt_this_frame(entity) {
state.apply_brushes_for(entity, &brushes.brushes);
}
for (name, target, readback) in state.poll_brush_reads_for(entity, &brushes.brushes) {
changed.write(NoesisBrushChanged {
view: entity,
name,
target,
readback,
});
}
}
}
pub struct NoesisBrushesPlugin;
impl Plugin for NoesisBrushesPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisBrushChanged>()
.add_systems(PostUpdate, sync_brushes_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_brushes() {
let b = NoesisBrushes::new()
.solid("Panel", BrushTarget::Background, [1.0, 0.0, 0.0, 1.0])
.linear_gradient(
"Bar",
BrushTarget::Fill,
[0.0, 0.0],
[1.0, 0.0],
vec![
GradientStop::new(0.0, [0.0, 0.0, 0.0, 1.0]),
GradientStop::new(1.0, [1.0, 1.0, 1.0, 1.0]),
],
);
assert_eq!(
b.brushes
.get(&("Panel".to_string(), BrushTarget::Background)),
Some(&BrushSpec::Solid([1.0, 0.0, 0.0, 1.0])),
);
assert!(matches!(
b.brushes.get(&("Bar".to_string(), BrushTarget::Fill)),
Some(BrushSpec::LinearGradient { stops, .. }) if stops.len() == 2,
));
}
#[test]
fn target_property_names() {
assert_eq!(BrushTarget::Background.property(), "Background");
assert_eq!(BrushTarget::Foreground.property(), "Foreground");
assert_eq!(BrushTarget::Fill.property(), "Fill");
assert_eq!(BrushTarget::Stroke.property(), "Stroke");
}
}