testing paralell

This commit is contained in:
2026-03-16 14:25:32 +01:00
parent 553ae724fc
commit da1ef93f8d
9 changed files with 445 additions and 178 deletions

50
Cargo.lock generated
View File

@@ -33,6 +33,7 @@ dependencies = [
"anyhow",
"config",
"ctrlc",
"futures",
"grim-rs",
"image",
"image-compare",
@@ -861,6 +862,21 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
@@ -868,6 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -876,6 +893,34 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.32"
@@ -894,8 +939,13 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"slab",
]

View File

@@ -7,6 +7,7 @@ edition = "2021"
anyhow = "1.0"
config = "0.15.21"
ctrlc = "3.5.2"
futures = "0.3"
grim-rs = "0.1.6"
image = "0.25.10"
image-compare = "0.5.0"

100
README.md
View File

@@ -78,3 +78,103 @@ nix develop -c cargo run --release
3. **Analyze:** Calculates the dominant color of the current frame using the Oklab color space.
4. **Smooth:** Blends the new dominant color with the previous one based on the `smoothing` factor.
5. **Update:** Constructs a highly specific JSON payload for Home Assistant, ensuring white channels and brightness are preserved, and sends it asynchronously via `reqwest`.
# TODO: Parallel Event Loop Restructuring
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ Shared State │
│ │
│ current_screenshot ← Latest captured (screenshot loop) │
│ active_screenshot ← Screenshot that produced target_color│
│ target_color ← Color calculated from active_screenshot│
│ active_color ← Smoothed color sent to lights │
└─────────────────────────────────────────────────────────────┘
Data Flow:
Screenshot Loop (screenshot_fps) Color Calc (on-demand)
│ │
▼ ▼
[current_screenshot] [target_color]
│ │
│ diff(current, active) │
│ > min_diff_percent? │
▼ │
[active_screenshot] ◄───────────────────────┘
│ │
│ Smoothing Loop (target_fps)│
│ │ │
│ ▼ │
│ [active_color] ◄─────────┘
│ │
▼ ▼
Light Update (parallel to all lights)
```
## Implementation Tasks
### Phase 1: Settings Update (`settings.rs`)
- Add `screenshot_fps` config option (default: 3)
- Remove `transition` option (smoothing handles transitions)
### Phase 2: Create Shared State (`state.rs`)
```rust
pub struct AppState {
pub current_screenshot: RwLock<Option<Arc<Screenshot>>>,
pub active_screenshot: RwLock<Option<Arc<Screenshot>>>,
pub target_color: RwLock<Option<Rgb<f32>>>,
pub active_color: RwLock<Rgb<f32>>,
}
```
### Phase 3: Parallel Event Loop (`eventloop.rs`)
**Thread 1 - Screenshot Loop** (std::thread, runs at `screenshot_fps`)
- Capture screenshot → store in `current_screenshot`
- Compare `current_screenshot` vs `active_screenshot`
- If diff > `min_diff_percent`:
- Copy `current_screenshot``active_screenshot`
- Spawn color calc task
**On-demand - Color Calculation** (tokio::spawn)
- Calculate dominant color from `active_screenshot`
- Update `target_color`
**Thread 2 - Smoothing & Light Update** (tokio, runs at `target_fps`)
- Lerp `active_color` toward `target_color` using `smoothing` factor
- Send `active_color` to all lights **in parallel**
### Phase 4: Parallel Light Updates (`homeassistant.rs`)
- Refactor `set_lights_color` to update single light, return future
- Create `set_lights_color_parallel` using `futures::future::join_all`
## Concurrency Strategy
| Resource | Lock Type | Writer | Readers |
|---------------------|-----------|---------------------|----------------------------|
| current_screenshot | RwLock | Screenshot loop | Diff check |
| active_screenshot | RwLock | Screenshot loop | Color calc |
| target_color | RwLock | Color calc task | Smoothing loop |
| active_color | RwLock | Smoothing loop | Light update |
Each field has a single writer - no complex synchronization needed.
## File Changes Summary
| File | Changes |
|-------------------|----------------------------------------------|
| `settings.rs` | Add `screenshot_fps`, remove `transition` |
| `state.rs` | **New** - Shared state struct |
| `eventloop.rs` | **Rewrite** - Multi-threaded architecture |
| `homeassistant.rs`| Add parallel light update method |
| `main.rs` | Wire up new state, pass to eventloop |

View File

@@ -1,31 +1,43 @@
# Configuration for Ambilight Home Assistant Integration
# # Configuration for Ambilight Home Assistant Integration
#
# # The base URL of your Home Assistant instance.
# # Example: "http://192.168.1.100:8123" or "https://ha.yourdomain.com"
# ha_url = "http://192.168.1.238:8123"
#
# # A Long-Lived Access Token from Home Assistant.
# # You can generate this in Home Assistant by going to your user profile.
# ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI0YjNjMmYxNGE3NWM0YmJjYmNhYzA0YWFmYTlhOTVkNCIsImlhdCI6MTc3MzUyOTcwNCwiZXhwIjoyMDg4ODg5NzA0fQ.I923kml1zWqYYZk-0JSunexbo5NKcgehhwmG-T8jBcg"
#
# # Target updates per second (FPS)
# target_fps = 3
#
# # Color smoothing factor (0.0 to 1.0)
# # 0.0 = no smoothing (instant updates)
# # 1.0 = completely static (no updates)
# smoothing = 0.1
#
# # Specific lights to control. If empty, all supported color lights are used.
# # Example: ["light.living_room", "light.bedroom"]
# lights = []
#
# # Restore lights to their original state/color when the program exits.
# restore_on_exit = true
#
# # Time (in seconds) it takes to fade to the new color. 0.0 means instant.
# transition = 0.1
#
# # Minimum percentage of difference required between frames to trigger an update (e.g. 1.0 = 1%)
# # Helps with flickering on static images, from the non deterministic color algorithm.
# min_diff_percent = 5.0
# The base URL of your Home Assistant instance.
# Example: "http://192.168.1.100:8123" or "https://ha.yourdomain.com"
ha_url = "http://192.168.1.238:8123"
# A Long-Lived Access Token from Home Assistant.
# You can generate this in Home Assistant by going to your user profile.
ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI0YjNjMmYxNGE3NWM0YmJjYmNhYzA0YWFmYTlhOTVkNCIsImlhdCI6MTc3MzUyOTcwNCwiZXhwIjoyMDg4ODg5NzA0fQ.I923kml1zWqYYZk-0JSunexbo5NKcgehhwmG-T8jBcg"
# Target updates per second (FPS)
target_fps = 3
# Color smoothing factor (0.0 to 1.0)
# 0.0 = no smoothing (instant updates)
# 1.0 = completely static (no updates)
smoothing = 0.1
# Specific lights to control. If empty, all supported color lights are used.
# Example: ["light.living_room", "light.bedroom"]
lights = []
# Restore lights to their original state/color when the program exits.
ha_url = "https://homeassistant.pvv.ntnu.no:8123"
ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3ZmIxMzQ4NzE5ZmU0MmMyYTA5M2JiNWU2NjQ2OWJmYSIsImlhdCI6MTc3MzY2MzQwNCwiZXhwIjoyMDg5MDIzNDA0fQ.JKn8hPvK7aPS-SuXKKEREk7vva-PGXehLUH7ijoSbjQ"
target_fps = 15.0
smoothing = 0.6
lights = [ "light.fargelys" ]
restore_on_exit = true
# Time (in seconds) it takes to fade to the new color. 0.0 means instant.
transition = 0.1
# Minimum percentage of difference required between frames to trigger an update (e.g. 1.0 = 1%)
# Helps with flickering on static images, from the non deterministic color algorithm.
min_diff_percent = 5.0
transition = 0.0
min_diff_percent = 10.0

View File

@@ -1,93 +1,148 @@
use crate::homeassistant::HaClient;
use crate::screenshot;
use crate::settings::Settings;
use crate::state::AppState;
use rgb::RGB;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use tokio::time::sleep;
pub async fn run_loop(
settings: &Settings,
ha_client: &HaClient,
target_lights: &[String],
pub fn run_loop(
settings: Arc<Settings>,
ha_client: Arc<HaClient>,
target_lights: Arc<Vec<String>>,
state: Arc<AppState>,
is_running: Arc<AtomicBool>,
mut exit_rx: mpsc::Receiver<()>,
) {
let fps = settings.target_fps.max(1);
let target_duration = Duration::from_millis((1000 / fps) as u64);
let rt = tokio::runtime::Runtime::new().unwrap();
let mut current_color: Option<(f32, f32, f32)> = None;
let mut last_screenshot: Option<screenshot::Screenshot> = None;
let screenshot_fps = settings.screenshot_fps.max(1);
let target_fps = settings.target_fps.max(1);
let screenshot_interval = Duration::from_millis((1000 / screenshot_fps) as u64);
let target_interval = Duration::from_millis((1000 / target_fps) as u64);
let smoothing = settings.smoothing.clamp(0.0, 1.0);
let min_diff_percent = settings.min_diff_percent;
println!("Starting Ambilight loop at {} FPS...", fps);
println!(
"Starting Ambilight loop (screenshot: {} FPS, light update: {} FPS)...",
screenshot_fps, target_fps
);
loop {
let start_time = Instant::now();
let is_running_screenshot = is_running.clone();
let state_screenshot = state.clone();
let settings_screenshot = settings.clone();
if is_running.load(Ordering::Relaxed) {
match screenshot::get_screenshot(0) {
Ok(current_screenshot) => {
let mut should_update = true;
let screenshot_handle = thread::spawn(move || {
loop {
let start = Instant::now();
if let Some(prev) = &last_screenshot {
let diff = current_screenshot.diff_percent(prev);
if diff < settings.min_diff_percent {
should_update = false;
if is_running_screenshot.load(Ordering::Relaxed) {
if let Ok(current) = screenshot::get_screenshot(0) {
let current_arc = Arc::new(current);
let should_update = {
let active = state_screenshot.active_screenshot.read().unwrap();
match active.as_ref() {
None => true,
Some(active_screenshot) => {
let diff = current_arc.diff_percent(active_screenshot);
diff >= settings_screenshot.min_diff_percent
}
}
}
};
if should_update {
let color = current_screenshot.dominant_color();
let (new_r, new_g, new_b) =
(color.r as f32, color.g as f32, color.b as f32);
let (r, g, b) = match current_color {
None => (new_r, new_g, new_b),
Some((cr, cg, cb)) => {
let s_factor = settings.smoothing.clamp(0.0, 1.0);
(
cr * s_factor + new_r * (1.0 - s_factor),
cg * s_factor + new_g * (1.0 - s_factor),
cb * s_factor + new_b * (1.0 - s_factor),
)
}
};
current_color = Some((r, g, b));
let (final_r, final_g, final_b) =
(r.round() as u8, g.round() as u8, b.round() as u8);
if let Err(e) = ha_client
.set_lights_color(
target_lights,
final_r,
final_g,
final_b,
settings.transition,
)
.await
{
eprintln!("Failed to update lights: {}", e);
let mut current_guard = state_screenshot.current_screenshot.write().unwrap();
*current_guard = Some(current_arc.clone());
}
{
let mut active_guard = state_screenshot.active_screenshot.write().unwrap();
*active_guard = Some(current_arc);
}
last_screenshot = Some(current_screenshot);
}
}
Err(e) => eprintln!("Screenshot error: {}", e),
}
let elapsed = start.elapsed();
let sleep_duration = screenshot_interval.saturating_sub(elapsed);
thread::sleep(sleep_duration);
}
});
let state_smoothing = state.clone();
let ha_client_smoothing = ha_client.clone();
let target_lights_smoothing = target_lights.clone();
let is_running_smoothing = is_running.clone();
rt.block_on(async {
let mut last_calc_time = Instant::now();
let calc_interval = Duration::from_millis(50);
loop {
let start = Instant::now();
if is_running_smoothing.load(Ordering::Relaxed) {
if start.duration_since(last_calc_time) >= calc_interval {
let active_screenshot = {
let guard = state_smoothing.active_screenshot.read().unwrap();
guard.clone()
};
if let Some(screenshot) = active_screenshot {
let color = screenshot.dominant_color();
let new_target = RGB::new(color.r as f32, color.g as f32, color.b as f32);
{
let mut target_guard = state_smoothing.target_color.write().unwrap();
*target_guard = Some(new_target);
}
}
last_calc_time = start;
}
{
let target_color = {
let guard = state_smoothing.target_color.read().unwrap();
*guard
};
if let Some(target) = target_color {
let mut active_guard = state_smoothing.active_color.write().unwrap();
active_guard.r = active_guard.r * smoothing + target.r * (1.0 - smoothing);
active_guard.g = active_guard.g * smoothing + target.g * (1.0 - smoothing);
active_guard.b = active_guard.b * smoothing + target.b * (1.0 - smoothing);
}
}
let (r, g, b) = {
let guard = state_smoothing.active_color.read().unwrap();
(guard.r.round() as u8, guard.g.round() as u8, guard.b.round() as u8)
};
if let Err(e) = ha_client_smoothing
.set_lights_color_parallel(&target_lights_smoothing, r, g, b)
.await
{
eprintln!("Failed to update lights: {}", e);
}
}
let elapsed = start.elapsed();
let sleep_duration = target_interval.saturating_sub(elapsed);
tokio::select! {
_ = tokio::time::sleep(sleep_duration) => {}
_ = exit_rx.recv() => {
println!("\nExit signal received, stopping loop...");
break;
}
}
}
});
// Sleep to maintain target FPS, saturating_sub prevents negative duration panics if we run slow
let elapsed = start_time.elapsed();
let sleep_duration = target_duration.saturating_sub(elapsed);
tokio::select! {
_ = sleep(sleep_duration) => {}
_ = exit_rx.recv() => {
println!("\nExit signal received, stopping loop...");
break;
}
}
}
let _ = screenshot_handle.join();
}

View File

@@ -90,77 +90,98 @@ impl HaClient {
Ok(())
}
pub async fn set_lights_color(
pub async fn set_light_color(
&self,
entity_id: &str,
r: u8,
g: u8,
b: u8,
states: &[HaState],
) -> Result<(), Error> {
let url = format!("{}/api/services/light/turn_on", self.base_url);
let mut payload = json!({
"entity_id": entity_id,
});
if let Some(state) = states.iter().find(|s| s.entity_id == entity_id) {
if let Some(brightness) = state.attributes.get("brightness") {
payload["brightness"] = brightness.clone();
}
let color_mode = state
.attributes
.get("color_mode")
.and_then(|m| m.as_str())
.unwrap_or("");
match color_mode {
"rgbw" => {
let w = state
.attributes
.get("rgbw_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(3))
.cloned()
.unwrap_or(json!(0));
payload["rgbw_color"] = json!([r, g, b, w]);
}
"rgbww" => {
let cw = state
.attributes
.get("rgbww_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(3))
.cloned()
.unwrap_or(json!(0));
let ww = state
.attributes
.get("rgbww_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(4))
.cloned()
.unwrap_or(json!(0));
payload["rgbww_color"] = json!([r, g, b, cw, ww]);
}
_ => {
payload["rgb_color"] = json!([r, g, b]);
}
}
} else {
payload["rgb_color"] = json!([r, g, b]);
}
self.client
.post(&url)
.bearer_auth(&self.token)
.json(&payload)
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn set_lights_color_parallel(
&self,
entity_ids: &[String],
r: u8,
g: u8,
b: u8,
transition: f32,
) -> Result<(), Error> {
let states = self.get_lights().await.unwrap_or_default();
let url = format!("{}/api/services/light/turn_on", self.base_url);
for entity_id in entity_ids {
let mut payload = json!({
"entity_id": entity_id,
"transition": transition,
});
let futures: Vec<_> = entity_ids
.iter()
.map(|entity_id| self.set_light_color(entity_id, r, g, b, &states))
.collect();
if let Some(state) = states.iter().find(|s| s.entity_id == *entity_id) {
if let Some(brightness) = state.attributes.get("brightness") {
payload["brightness"] = brightness.clone();
}
let results = futures::future::join_all(futures).await;
let color_mode = state
.attributes
.get("color_mode")
.and_then(|m| m.as_str())
.unwrap_or("");
match color_mode {
"rgbw" => {
let w = state
.attributes
.get("rgbw_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(3))
.cloned()
.unwrap_or(json!(0));
payload["rgbw_color"] = json!([r, g, b, w]);
}
"rgbww" => {
let cw = state
.attributes
.get("rgbww_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(3))
.cloned()
.unwrap_or(json!(0));
let ww = state
.attributes
.get("rgbww_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(4))
.cloned()
.unwrap_or(json!(0));
payload["rgbww_color"] = json!([r, g, b, cw, ww]);
}
_ => {
payload["rgb_color"] = json!([r, g, b]);
}
}
} else {
payload["rgb_color"] = json!([r, g, b]);
for result in results {
if let Err(e) = result {
eprintln!("Failed to update light: {}", e);
}
let _ = self
.client
.post(&url)
.bearer_auth(&self.token)
.json(&payload)
.send()
.await;
}
Ok(())

View File

@@ -2,6 +2,7 @@ mod eventloop;
mod homeassistant;
mod screenshot;
mod settings;
mod state;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@@ -48,32 +49,34 @@ fn main() {
let is_running = Arc::new(AtomicBool::new(true));
let (exit_tx, exit_rx) = tokio::sync::mpsc::channel(1);
// Leak variables to give them a 'static lifetime for the thread
let settings_ref = &*Box::leak(Box::new(settings));
let ha_client_ref = &*Box::leak(Box::new(ha_client));
let target_lights_ref = &*Box::leak(Box::new(target_lights));
let settings_arc = Arc::new(settings);
let ha_client_arc = Arc::new(ha_client);
let target_lights_arc = Arc::new(target_lights);
let state_arc = Arc::new(state::AppState::new());
let is_running_clone = is_running.clone();
let settings_arc_clone = settings_arc.clone();
let ha_client_arc_clone = ha_client_arc.clone();
let target_lights_arc_clone = target_lights_arc.clone();
// Spawn the main application event loop on a separate thread
let app_thread = std::thread::spawn(move || {
let rt2 = tokio::runtime::Runtime::new().unwrap();
rt2.block_on(eventloop::run_loop(
settings_ref,
ha_client_ref,
target_lights_ref,
eventloop::run_loop(
settings_arc_clone,
ha_client_arc_clone,
target_lights_arc_clone,
state_arc,
is_running_clone,
exit_rx,
));
);
});
// Create the tray application
let mut tray = TrayItem::new("Ambiligth", IconSource::Resource("")).unwrap();
let is_running_toggle = is_running.clone();
let restore_on_toggle = settings_ref.restore_on_exit;
let ha_client_toggle = ha_client_ref;
let target_lights_toggle = target_lights_ref;
let restore_on_toggle = settings_arc.restore_on_exit;
let ha_client_toggle = ha_client_arc.clone();
let target_lights_toggle = target_lights_arc.clone();
let rt_handle_toggle = rt_handle.clone();
tray.add_menu_item("Toggle Ambilight", move || {
@@ -98,7 +101,7 @@ fn main() {
println!("Creating scene snapshot for restore on exit...");
if let Err(e) = rt_handle_toggle.block_on(
ha_client_toggle
.create_scene_snapshot("ambilight_restore", target_lights_toggle),
.create_scene_snapshot("ambilight_restore", &*target_lights_toggle),
) {
eprintln!("Failed to create scene snapshot: {}", e);
}
@@ -121,16 +124,14 @@ fn main() {
})
.expect("Error setting Ctrl-C handler");
// Block the main thread until a quit signal is received
let _ = sync_exit_rx.recv();
println!("Shutting down...");
// Stop the event loop
let _ = exit_tx.try_send(());
if settings_ref.restore_on_exit {
if settings_arc.restore_on_exit {
println!("Restoring lights to original state...");
if let Err(e) = rt.block_on(ha_client_ref.turn_on_scene("scene.ambilight_restore")) {
if let Err(e) = rt.block_on(ha_client_arc.turn_on_scene("scene.ambilight_restore")) {
eprintln!("Failed to restore scene: {}", e);
} else {
println!("Lights restored successfully.");

View File

@@ -12,6 +12,9 @@ pub struct Settings {
#[serde(default = "default_target_fps")]
pub target_fps: u32,
#[serde(default = "default_screenshot_fps")]
pub screenshot_fps: u32,
#[serde(default = "default_smoothing")]
pub smoothing: f32,
@@ -21,9 +24,6 @@ pub struct Settings {
#[serde(default = "default_restore_on_exit")]
pub restore_on_exit: bool,
#[serde(default = "default_transition")]
pub transition: f32,
#[serde(default = "default_min_diff_percent")]
pub min_diff_percent: f32,
}
@@ -32,10 +32,6 @@ fn default_min_diff_percent() -> f32 {
1.0
}
fn default_transition() -> f32 {
0.5
}
fn default_restore_on_exit() -> bool {
true
}
@@ -44,6 +40,10 @@ fn default_target_fps() -> u32 {
1
}
fn default_screenshot_fps() -> u32 {
3
}
fn default_smoothing() -> f32 {
0.0
}

27
src/state.rs Normal file
View File

@@ -0,0 +1,27 @@
use crate::screenshot::Screenshot;
use rgb::RGB;
use std::sync::{Arc, RwLock};
pub struct AppState {
pub current_screenshot: RwLock<Option<Arc<Screenshot>>>,
pub active_screenshot: RwLock<Option<Arc<Screenshot>>>,
pub target_color: RwLock<Option<RGB<f32>>>,
pub active_color: RwLock<RGB<f32>>,
}
impl AppState {
pub fn new() -> Self {
Self {
current_screenshot: RwLock::new(None),
active_screenshot: RwLock::new(None),
target_color: RwLock::new(None),
active_color: RwLock::new(RGB::new(0.0, 0.0, 0.0)),
}
}
}
impl Default for AppState {
fn default() -> Self {
Self::new()
}
}