Implement HSB picker for lighting
Diff
Cargo.lock | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
shalom/Cargo.toml | 2 +-
shalom/src/hass_client.rs | 26 +++++++++++++-------------
shalom/src/main.rs | 44 ++++++++++++++++++++++----------------------
shalom/src/oracle.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
shalom/src/context_menus/light_control.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
shalom/src/context_menus/mod.rs | 1 +
shalom/src/pages/room.rs | 24 ++++++++++++++++++------
shalom/src/widgets/colour_picker.rs | 384 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
shalom/src/widgets/mod.rs | 1 +
shalom/src/widgets/toggle_card.rs | 10 +++++-----
11 files changed, 661 insertions(+), 64 deletions(-)
@@ -688,6 +688,15 @@
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
name = "float_next_after"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632"
dependencies = [
"num-traits",
]
[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1194,6 +1203,7 @@
"image",
"kamadak-exif",
"log",
"lyon_path",
"raw-window-handle",
"thiserror",
]
@@ -1267,6 +1277,7 @@
"guillotiere",
"iced_graphics",
"log",
"lyon",
"once_cell",
"raw-window-handle",
"resvg",
@@ -1565,6 +1576,58 @@
checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60"
dependencies = [
"hashbrown 0.14.2",
]
[[package]]
name = "lyon"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f"
dependencies = [
"lyon_algorithms",
"lyon_tessellation",
]
[[package]]
name = "lyon_algorithms"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00a0349cd8f0270781bb93a824b63df6178e3b4a27794e7be3ce3763f5a44d6e"
dependencies = [
"lyon_path",
"num-traits",
]
[[package]]
name = "lyon_geom"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74df1ff0a0147282eb10699537a03baa7d31972b58984a1d44ce0624043fe8ad"
dependencies = [
"arrayvec",
"euclid",
"num-traits",
]
[[package]]
name = "lyon_path"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca507745ba7ccbc76e5c44e7b63b1a29d2b0d6126f375806a5bbaf657c7d6c45"
dependencies = [
"lyon_geom",
"num-traits",
]
[[package]]
name = "lyon_tessellation"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d2124218d5428149f9e09520b9acc024334a607e671f032d06567b61008977c"
dependencies = [
"float_next_after",
"lyon_path",
"thiserror",
]
[[package]]
@@ -6,7 +6,7 @@
[dependencies]
iced = { version = "0.10", features = ["tokio", "svg", "lazy", "advanced", "image"] }
iced = { version = "0.10", features = ["tokio", "svg", "lazy", "advanced", "image", "canvas"] }
image = "0.24"
once_cell = "1.18"
internment = "0.7.4"
@@ -619,23 +619,23 @@
#[derive(Deserialize, Debug, Clone)]
pub struct StateLightAttributes<'a> {
min_color_temp_kelvin: Option<u16>,
max_color_temp_kelvin: Option<u16>,
min_mireds: Option<u16>,
max_mireds: Option<u16>,
pub min_color_temp_kelvin: Option<u16>,
pub max_color_temp_kelvin: Option<u16>,
pub min_mireds: Option<u16>,
pub max_mireds: Option<u16>,
#[serde(default)]
supported_color_modes: Vec<ColorMode>,
pub supported_color_modes: Vec<ColorMode>,
#[serde(borrow)]
mode: Option<Cow<'a, str>>,
pub mode: Option<Cow<'a, str>>,
#[serde(borrow)]
dynamics: Option<Cow<'a, str>>,
pub dynamics: Option<Cow<'a, str>>,
#[serde(borrow)]
friendly_name: Cow<'a, str>,
color_mode: Option<ColorMode>,
brightness: Option<f32>,
color_temp_kelvin: Option<u16>,
color_temp: Option<u16>,
xy_color: Option<(f32, f32)>,
pub friendly_name: Cow<'a, str>,
pub color_mode: Option<ColorMode>,
pub brightness: Option<f32>,
pub color_temp_kelvin: Option<u16>,
pub color_temp: Option<u16>,
pub xy_color: Option<(f32, f32)>,
}
#[derive(Deserialize, Debug, Clone, Copy)]
@@ -1,6 +1,7 @@
#![deny(clippy::pedantic)]
mod config;
mod context_menus;
mod hass_client;
mod oracle;
mod pages;
@@ -12,10 +13,9 @@
use iced::{
alignment::{Horizontal, Vertical},
font::{Stretch, Weight},
widget::{column, container, row, scrollable, svg, text, vertical_slider, Column},
window, Alignment, Application, Command, ContentFit, Element, Font, Length, Renderer, Settings,
Subscription, Theme,
widget::{column, container, row, scrollable, svg, Column},
window, Application, Command, ContentFit, Element, Length, Renderer, Settings, Subscription,
Theme,
};
use crate::{
@@ -66,21 +66,21 @@
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
#[allow(clippy::single_match)]
match (message, &mut self.page) {
(Message::Loaded(oracle), _) => {
match (message, &mut self.page, &mut self.context_menu) {
(Message::Loaded(oracle), _, _) => {
self.oracle = Some(oracle);
self.page = ActivePage::Room(pages::room::Room::new(
"living_room",
self.oracle.clone().unwrap(),
));
}
(Message::CloseContextMenu, _) => {
(Message::CloseContextMenu, _, _) => {
self.context_menu = None;
}
(Message::OpenOmniPage, _) => {
(Message::OpenOmniPage, _, _) => {
self.page = ActivePage::Omni(pages::omni::Omni::new(self.oracle.clone().unwrap()));
}
(Message::OmniEvent(e), ActivePage::Omni(r)) => match r.update(e) {
(Message::OmniEvent(e), ActivePage::Omni(r), _) => match r.update(e) {
Some(pages::omni::Event::OpenRoom(room)) => {
self.page = ActivePage::Room(pages::room::Room::new(
room,
@@ -89,12 +89,19 @@
}
None => {}
},
(Message::RoomEvent(e), ActivePage::Room(r)) => match r.update(e) {
(Message::RoomEvent(e), ActivePage::Room(r), _) => match r.update(e) {
Some(pages::room::Event::OpenLightContextMenu(light)) => {
self.context_menu = Some(ActiveContextMenu::LightOptions(light));
self.context_menu = Some(ActiveContextMenu::LightControl(
context_menus::light_control::LightControl::new(light),
));
}
None => {}
},
(Message::LightControlMenu(e), _, Some(ActiveContextMenu::LightControl(menu))) => {
match menu.update(e) {
Some(_) | None => {}
}
}
_ => {}
}
@@ -164,17 +171,7 @@
if let Some(context_menu) = &self.context_menu {
let context_menu = match context_menu {
ActiveContextMenu::LightOptions(name) => container(column![
text(name).size(40).font(Font {
weight: Weight::Bold,
stretch: Stretch::Condensed,
..Font::with_name("Helvetica Neue")
}),
row![vertical_slider(0..=100, 0, |_v| Message::CloseContextMenu).height(200)]
.align_items(Alignment::Center)
])
.width(Length::Fill)
.padding(40),
ActiveContextMenu::LightControl(menu) => menu.view().map(Message::LightControlMenu),
};
ContextMenu::new(content, context_menu)
@@ -206,6 +203,7 @@
OpenOmniPage,
OmniEvent(pages::omni::Message),
RoomEvent(pages::room::Message),
LightControlMenu(context_menus::light_control::Message),
}
#[derive(Debug)]
@@ -218,7 +216,7 @@
#[derive(Clone, Debug)]
pub enum ActiveContextMenu {
LightOptions(&'static str),
LightControl(context_menus::light_control::LightControl),
}
fn main() {
@@ -1,5 +1,6 @@
use std::{
collections::{BTreeMap, HashMap},
borrow::Cow,
collections::{BTreeMap, BTreeSet, HashMap},
str::FromStr,
sync::{Arc, Mutex},
time::Duration,
@@ -14,8 +15,9 @@
use crate::hass_client::{
responses::{
Area, AreaRegistryList, DeviceRegistryList, Entity, EntityRegistryList, StateAttributes,
StateMediaPlayerAttributes, StateWeatherAttributes, StatesList, WeatherCondition,
Area, AreaRegistryList, ColorMode, DeviceRegistryList, Entity, EntityRegistryList,
StateAttributes, StateLightAttributes, StateMediaPlayerAttributes, StateWeatherAttributes,
StatesList, WeatherCondition,
},
Event, HassRequestKind,
};
@@ -25,8 +27,9 @@
pub struct Oracle {
client: crate::hass_client::Client,
rooms: BTreeMap<&'static str, Room>,
pub weather: Mutex<Weather>,
pub media_players: Mutex<BTreeMap<&'static str, MediaPlayer>>,
weather: Mutex<Weather>,
media_players: Mutex<BTreeMap<&'static str, MediaPlayer>>,
lights: Mutex<BTreeMap<&'static str, Light>>,
entity_updates: broadcast::Sender<Arc<str>>,
}
@@ -61,18 +64,26 @@
eprintln!("{rooms:#?}");
let media_players = states
.0
.iter()
.filter_map(|state| {
if let StateAttributes::MediaPlayer(attr) = &state.attributes {
let kind = MediaPlayer::new(attr, &hass_client.base);
Some((Intern::<str>::from(state.entity_id.as_ref()).as_ref(), kind))
} else {
None
let mut media_players = BTreeMap::new();
let mut lights = BTreeMap::new();
for state in &states.0 {
match &state.attributes {
StateAttributes::MediaPlayer(attr) => {
media_players.insert(
Intern::<str>::from(state.entity_id.as_ref()).as_ref(),
MediaPlayer::new(attr, &hass_client.base),
);
}
})
.collect();
StateAttributes::Light(attr) => {
lights.insert(
Intern::<str>::from(state.entity_id.as_ref()).as_ref(),
Light::from(attr.clone()),
);
}
_ => {}
}
}
let (entity_updates, _) = broadcast::channel(10);
@@ -81,6 +92,7 @@
rooms,
weather: Mutex::new(Weather::parse_from_states(states)),
media_players: Mutex::new(media_players),
lights: Mutex::new(lights),
entity_updates: entity_updates.clone(),
});
@@ -177,12 +189,19 @@
})
.find(|v| v.starts_with("media_player."))
.copied();
let lights = entities
.iter()
.filter(|v| v.starts_with("light."))
.copied()
.collect();
let area = Intern::<str>::from(room.area_id.as_ref()).as_ref();
let room = Room {
name: Intern::from(room.name.as_ref()),
entities,
speaker_id,
lights,
};
(area, room)
@@ -216,6 +235,43 @@
})
} else {
MediaPlayer::Tv(MediaPlayerTv {})
}
}
}
#[derive(Debug, Clone)]
pub struct Light {
pub min_color_temp_kelvin: Option<u16>,
pub max_color_temp_kelvin: Option<u16>,
pub min_mireds: Option<u16>,
pub max_mireds: Option<u16>,
pub supported_color_modes: Vec<ColorMode>,
pub mode: Option<Box<str>>,
pub dynamics: Option<Box<str>>,
pub friendly_name: Box<str>,
pub color_mode: Option<ColorMode>,
pub brightness: Option<f32>,
pub color_temp_kelvin: Option<u16>,
pub color_temp: Option<u16>,
pub xy_color: Option<(f32, f32)>,
}
impl From<StateLightAttributes<'_>> for Light {
fn from(value: StateLightAttributes<'_>) -> Self {
Self {
min_color_temp_kelvin: value.min_color_temp_kelvin,
max_color_temp_kelvin: value.max_color_temp_kelvin,
min_mireds: value.min_mireds,
max_mireds: value.max_mireds,
supported_color_modes: value.supported_color_modes.clone(),
mode: value.mode.map(Cow::into_owned).map(Box::from),
dynamics: value.dynamics.map(Cow::into_owned).map(Box::from),
friendly_name: Box::from(value.friendly_name.as_ref()),
color_mode: value.color_mode,
brightness: value.brightness,
color_temp_kelvin: value.color_temp_kelvin,
color_temp: value.color_temp,
xy_color: value.xy_color,
}
}
}
@@ -243,6 +299,7 @@
pub name: Intern<str>,
pub entities: Vec<Intern<str>>,
pub speaker_id: Option<Intern<str>>,
pub lights: BTreeSet<Intern<str>>,
}
impl Room {
@@ -258,6 +315,19 @@
MediaPlayer::Speaker(v) => Some(v),
MediaPlayer::Tv(_) => None,
}
}
pub fn light_names(&self, oracle: &Oracle) -> BTreeMap<&'static str, Box<str>> {
let lights = oracle.lights.lock().unwrap();
self.lights
.iter()
.filter_map(|v| Some((*v).as_ref()).zip(lights.get(v.as_ref())))
.map(|(id, light)| {
eprintln!("{light:?}");
(id, light.friendly_name.clone())
})
.collect()
}
}
@@ -1,0 +1,68 @@
use iced::{
font::{Stretch, Weight},
widget::{column, container, row, text},
Alignment, Element, Font, Length, Renderer,
};
use crate::widgets::colour_picker::ColourPicker;
#[derive(Debug, Clone)]
pub struct LightControl {
id: &'static str,
hue: f32,
saturation: f32,
brightness: f32,
}
impl LightControl {
pub fn new(id: &'static str) -> Self {
Self {
id,
hue: 0.0,
saturation: 0.0,
brightness: 0.0,
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn update(&mut self, event: Message) -> Option<Event> {
match event {
Message::OnColourChange(hue, saturation, brightness) => {
self.hue = hue;
self.saturation = saturation;
self.brightness = brightness;
None
}
}
}
pub fn view(&self) -> Element<'_, Message, Renderer> {
let colour_picker = ColourPicker::new(
self.hue,
self.saturation,
self.brightness,
Message::OnColourChange,
);
container(column![
text(self.id).size(40).font(Font {
weight: Weight::Bold,
stretch: Stretch::Condensed,
..Font::with_name("Helvetica Neue")
}),
row![colour_picker,]
.align_items(Alignment::Center)
.spacing(20)
])
.width(Length::Fill)
.padding(40)
.into()
}
}
pub enum Event {}
#[derive(Clone, Debug)]
pub enum Message {
OnColourChange(f32, f32, f32),
}
@@ -1,0 +1,1 @@
pub mod light_control;
@@ -1,11 +1,11 @@
use std::sync::Arc;
use std::{collections::BTreeMap, sync::Arc};
use iced::{
advanced::graphics::core::Element,
font::{Stretch, Weight},
futures::StreamExt,
subscription,
widget::{container, image::Handle, row, text, Column},
widget::{container, image::Handle, text, Column, Row},
Font, Renderer, Subscription,
};
use internment::Intern;
@@ -24,6 +24,7 @@
room: crate::oracle::Room,
speaker: Option<MediaPlayerSpeaker>,
now_playing_image: Option<Handle>,
lights: BTreeMap<&'static str, Box<str>>,
}
impl Room {
@@ -31,11 +32,14 @@
let room = oracle.room(id).clone();
let speaker = room.speaker(&oracle);
let lights = room.light_names(&oracle);
Self {
oracle,
room,
speaker,
now_playing_image: None,
lights,
}
}
@@ -98,11 +102,11 @@
..Font::with_name("Helvetica Neue")
});
let light = |name| {
let light = |id, name| {
widgets::toggle_card::toggle_card(name, false)
.icon(Icon::Bulb)
.on_press(Message::LightToggle(name))
.on_long_press(Message::OpenLightOptions(name))
.on_press(Message::LightToggle(id))
.on_long_press(Message::OpenLightOptions(id))
};
let mut col = Column::new().spacing(20).padding(40).push(header);
@@ -117,7 +121,15 @@
);
}
col = col.push(row![light("Main"), light("Lamp"), light("TV")].spacing(10));
let lights = Row::with_children(
self.lights
.iter()
.map(|(id, name)| light(*id, name))
.map(Element::from)
.collect::<Vec<_>>(),
)
.spacing(10);
col = col.push(lights);
col.into()
}
@@ -1,0 +1,384 @@
use iced::{
advanced::graphics::core::Element,
event::Status,
mouse,
mouse::{Button, Cursor},
touch,
widget::{
canvas,
canvas::{Cache, Event, Frame, Geometry, Path, Stroke, Style},
component, Column, Component,
},
Color, Point, Rectangle, Renderer, Size, Theme,
};
pub struct ColourPicker<Event> {
hue: f32,
saturation: f32,
brightness: f32,
on_change: fn(f32, f32, f32) -> Event,
}
impl<Event> ColourPicker<Event> {
pub fn new(
hue: f32,
saturation: f32,
brightness: f32,
on_change: fn(f32, f32, f32) -> Event,
) -> Self {
Self {
hue,
saturation,
brightness,
on_change,
}
}
}
impl<Event> Component<Event, Renderer> for ColourPicker<Event> {
type State = ();
type Event = Message;
fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option<Event> {
match event {
Message::OnSaturationBrightnessChange(saturation, brightness) => {
Some((self.on_change)(self.hue, saturation, brightness))
}
Message::OnHueChanged(hue) => {
Some((self.on_change)(hue, self.saturation, self.brightness))
}
}
}
fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> {
let saturation_brightness_picker = canvas(SaturationBrightnessPicker::new(
self.hue,
self.saturation,
self.brightness,
Message::OnSaturationBrightnessChange,
))
.height(192)
.width(192);
let hue_slider = canvas(HueSlider::new(self.hue, Message::OnHueChanged))
.height(24)
.width(192);
Column::new()
.push(saturation_brightness_picker)
.push(hue_slider)
.spacing(4)
.into()
}
}
impl<'a, M> From<ColourPicker<M>> for Element<'a, M, Renderer>
where
M: 'a + Clone,
{
fn from(card: ColourPicker<M>) -> Self {
component(card)
}
}
#[derive(Clone)]
pub enum Message {
OnSaturationBrightnessChange(f32, f32),
OnHueChanged(f32),
}
pub struct HueSlider<Message> {
hue: f32,
on_hue_change: fn(f32) -> Message,
}
impl<Message> HueSlider<Message> {
fn new(hue: f32, on_hue_change: fn(f32) -> Message) -> Self {
Self { hue, on_hue_change }
}
}
impl<Message> canvas::Program<Message> for HueSlider<Message> {
type State = HueSliderState;
fn update(
&self,
state: &mut Self::State,
event: Event,
bounds: Rectangle,
cursor: Cursor,
) -> (Status, Option<Message>) {
let update = match event {
Event::Mouse(mouse::Event::ButtonPressed(Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. })
if cursor.is_over(bounds) =>
{
state.is_dragging = true;
true
}
Event::Mouse(mouse::Event::ButtonReleased(Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
state.is_dragging = false;
false
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. })
if state.is_dragging =>
{
true
}
_ => false,
};
if update {
if let Some(position) = cursor.position_in(bounds) {
state.arrow_cache.clear();
let hue = (position.x / bounds.width) * 360.;
(Status::Captured, Some((self.on_hue_change)(hue)))
} else {
(Status::Captured, None)
}
} else {
(Status::Ignored, None)
}
}
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: Cursor,
) -> Vec<Geometry> {
let content = state
.preview_cache
.draw(renderer, bounds.size(), |frame: &mut Frame| {
let size = frame.size();
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
for x in 0..size.width as u32 {
let hue = (x as f32 / size.width) * 360.0;
let color = colour_from_hsb(hue, 1.0, 1.0);
frame.fill_rectangle(
Point::new(x as f32, 0.0),
Size::new(1.0, size.height),
color,
);
}
});
let arrow = state
.arrow_cache
.draw(renderer, bounds.size(), |frame: &mut Frame| {
let size = frame.size();
let arrow_width = 10.0;
let arrow_height = 10.0;
let arrow_x = (self.hue / 360.0) * size.width - (arrow_width / 2.0);
let arrow_y = size.height - arrow_height;
let arrow = Path::new(|p| {
p.move_to(Point::new(arrow_x, arrow_y));
p.line_to(Point::new(arrow_x + arrow_width, arrow_y));
p.line_to(Point::new(
arrow_x + (arrow_width / 2.0),
arrow_y + arrow_height,
));
p.line_to(Point::new(arrow_x, arrow_y));
p.close();
});
frame.fill(&arrow, Color::BLACK);
});
vec![content, arrow]
}
}
#[derive(Default)]
pub struct HueSliderState {
is_dragging: bool,
preview_cache: Cache,
arrow_cache: Cache,
}
pub struct SaturationBrightnessPicker<Message> {
hue: f32,
saturation: f32,
brightness: f32,
on_change: fn(f32, f32) -> Message,
}
impl<Message> SaturationBrightnessPicker<Message> {
pub fn new(
hue: f32,
saturation: f32,
brightness: f32,
on_change: fn(f32, f32) -> Message,
) -> Self {
Self {
hue,
saturation,
brightness,
on_change,
}
}
}
impl<Message> canvas::Program<Message> for SaturationBrightnessPicker<Message> {
type State = SaturationBrightnessPickerState;
fn update(
&self,
state: &mut Self::State,
event: Event,
bounds: Rectangle,
cursor: Cursor,
) -> (Status, Option<Message>) {
#[allow(clippy::float_cmp)]
if self.hue != state.hue {
state.hue = self.hue;
state.content_cache.clear();
}
let update = match event {
Event::Mouse(mouse::Event::ButtonPressed(Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. })
if cursor.is_over(bounds) =>
{
state.is_dragging = true;
true
}
Event::Mouse(mouse::Event::ButtonReleased(Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
state.is_dragging = false;
false
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. })
if state.is_dragging =>
{
true
}
_ => false,
};
if update {
if let Some(position) = cursor.position_in(bounds) {
state.circle_cache.clear();
let saturation = position.x / bounds.width;
let brightness = 1.0 - (position.y / bounds.height);
(
Status::Captured,
Some((self.on_change)(saturation, brightness)),
)
} else {
(Status::Ignored, None)
}
} else {
(Status::Ignored, None)
}
}
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: Cursor,
) -> Vec<Geometry> {
let content = state
.content_cache
.draw(renderer, bounds.size(), |frame: &mut Frame| {
let size = frame.size();
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
for x in 0..size.width as u32 {
for y in 0..size.height as u32 {
let saturation = x as f32 / size.width;
let brightness = 1.0 - (y as f32 / size.height);
let color = colour_from_hsb(self.hue, saturation, brightness);
frame.fill_rectangle(
Point::new(x as f32, y as f32),
Size::new(1.0, 1.0),
color,
);
}
}
});
let circle = state
.circle_cache
.draw(renderer, bounds.size(), |frame: &mut Frame| {
let size = frame.size();
let circle_x = self.saturation * size.width;
let circle_y = (1.0 - self.brightness) * size.height;
let circle_radius = 5.0;
let circle = Path::circle(Point::new(circle_x, circle_y), circle_radius);
frame.stroke(
&circle,
Stroke {
style: Style::Solid(Color::BLACK),
width: 1.,
..Stroke::default()
},
);
});
vec![content, circle]
}
}
#[derive(Default)]
pub struct SaturationBrightnessPickerState {
is_dragging: bool,
content_cache: Cache,
circle_cache: Cache,
hue: f32,
}
fn colour_from_hsb(hue: f32, saturation: f32, brightness: f32) -> Color {
let chroma = brightness * saturation;
let hue_prime = hue / 60.0;
let second_largest_component = chroma * (1.0 - (hue_prime % 2.0 - 1.0).abs());
let match_value = brightness - chroma;
let (red, green, blue) = if hue < 60.0 {
(chroma, second_largest_component, 0.0)
} else if hue < 120.0 {
(second_largest_component, chroma, 0.0)
} else if hue < 180.0 {
(0.0, chroma, second_largest_component)
} else if hue < 240.0 {
(0.0, second_largest_component, chroma)
} else if hue < 300.0 {
(second_largest_component, 0.0, chroma)
} else {
(chroma, 0.0, second_largest_component)
};
Color::from_rgb(red + match_value, green + match_value, blue + match_value)
}
@@ -1,4 +1,5 @@
pub mod cards;
pub mod colour_picker;
pub mod context_menu;
pub mod image_card;
pub mod media_player;
@@ -20,9 +20,9 @@
pub const LONG_PRESS_LENGTH: Duration = Duration::from_millis(350);
pub fn toggle_card<M>(name: &'static str, active: bool) -> ToggleCard<M> {
pub fn toggle_card<M>(name: &str, active: bool) -> ToggleCard<M> {
ToggleCard {
name,
name: Box::from(name),
active,
..ToggleCard::default()
}
@@ -30,7 +30,7 @@
pub struct ToggleCard<M> {
icon: Option<Icon>,
name: &'static str,
name: Box<str>,
height: Length,
width: Length,
active: bool,
@@ -42,7 +42,7 @@
fn default() -> Self {
Self {
icon: None,
name: "",
name: Box::from(""),
height: Length::Shrink,
width: Length::Fill,
active: false,
@@ -125,7 +125,7 @@
.style(Svg::Custom(Box::new(style)))
});
let name = text(self.name).size(14).font(Font {
let name = text(&self.name).size(14).font(Font {
weight: Weight::Bold,
stretch: Stretch::Condensed,
..Font::with_name("Helvetica Neue")