Merge branch 'main' into script-to-host-messages

This commit is contained in:
mars 2022-11-20 00:15:36 -07:00
commit 9d4d5cca91
19 changed files with 682 additions and 253 deletions

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use canary::{DrawCommand, Vec2, PX_PER_MM}; use canary::{DrawCommand, Vec2, PX_PER_MM};
use glium::Surface; use glium::{program::ProgramCreationInput, Surface};
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Vertex { pub struct Vertex {
@ -59,9 +59,21 @@ pub struct Graphics {
impl Graphics { impl Graphics {
pub fn new(display: glium::Display) -> Self { pub fn new(display: glium::Display) -> Self {
let program = let program = glium::Program::new(
glium::Program::from_source(&display, VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC, None) &display,
.unwrap(); ProgramCreationInput::SourceCode {
vertex_shader: VERTEX_SHADER_SRC,
tessellation_control_shader: None,
tessellation_evaluation_shader: None,
geometry_shader: None,
fragment_shader: FRAGMENT_SHADER_SRC,
transform_feedback_varyings: None,
outputs_srgb: true, // don't automatically apply gamma correction
uses_point_size: false,
},
)
.unwrap();
Self { display, program } Self { display, program }
} }
@ -105,7 +117,7 @@ impl Graphics {
}; };
let mut target = self.display.draw(); let mut target = self.display.draw();
target.clear_color(0.0, 0.0, 0.0, 1.0); target.clear_color(0.0, 0.0, 0.0, 0.0);
target target
.draw( .draw(
&vertex_buffer, &vertex_buffer,

View File

@ -51,7 +51,9 @@ impl Window {
panel: Panel, panel: Panel,
event_loop: &EventLoopWindowTarget<WindowMessage>, event_loop: &EventLoopWindowTarget<WindowMessage>,
) -> Result<Self, DisplayCreationError> { ) -> Result<Self, DisplayCreationError> {
let wb = glutin::window::WindowBuilder::new(); let wb = glutin::window::WindowBuilder::new()
.with_transparent(true)
.with_decorations(false);
let cb = glutin::ContextBuilder::new(); let cb = glutin::ContextBuilder::new();
let display = glium::Display::new(wb, cb, &event_loop)?; let display = glium::Display::new(wb, cb, &event_loop)?;
let graphics = Graphics::new(display); let graphics = Graphics::new(display);

View File

@ -11,9 +11,11 @@ required-features = ["bin"]
[dependencies] [dependencies]
canary-magpie = { path = "../magpie", optional = true } canary-magpie = { path = "../magpie", optional = true }
mpris = { version = "2.0.0-rc3", optional = true } futures-util = { version = "0.3", optional = true }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
smol = { version = "1.2", optional = true }
zbus = { version = "3.5", optional = true }
[features] [features]
bin = ["dep:canary-magpie", "dep:mpris"] bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"]

View File

@ -34,9 +34,6 @@ pub enum LoopStatus {
pub struct ProgressChanged { pub struct ProgressChanged {
/// Current position into the track in seconds. /// Current position into the track in seconds.
pub position: f32, pub position: f32,
/// Length of the current track in seconds.
pub length: Option<f32>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -48,7 +45,7 @@ pub struct AlbumInfo {
pub artists: Vec<String>, pub artists: Vec<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct TrackInfo { pub struct TrackInfo {
/// The title of the current track. /// The title of the current track.
pub title: Option<String>, pub title: Option<String>,
@ -58,6 +55,9 @@ pub struct TrackInfo {
/// The optional track number on the disc the album the track appears on. /// The optional track number on the disc the album the track appears on.
pub track_number: Option<i32>, pub track_number: Option<i32>,
/// Length of the track in seconds.
pub length: Option<f32>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]

View File

@ -5,58 +5,63 @@ use std::{os::unix::net::UnixStream, path::Path};
use canary_magpie::protocol::{ClientMessenger, CreatePanel, MagpieServerMsg, MAGPIE_SOCK}; use canary_magpie::protocol::{ClientMessenger, CreatePanel, MagpieServerMsg, MAGPIE_SOCK};
use canary_music_player::*; use canary_music_player::*;
use mpris::PlayerFinder;
pub type MagpieClient<'a> = ClientMessenger<&'a UnixStream>; pub type MagpieClient<'a> = ClientMessenger<&'a UnixStream>;
pub struct MetadataTracker { pub mod mpris;
use mpris::*;
#[derive(Debug)]
pub struct Metadata {
pub album: AlbumInfo, pub album: AlbumInfo,
pub track: TrackInfo, pub track: TrackInfo,
} }
impl From<&mpris::Metadata> for MetadataTracker { impl<'a> From<MetadataMap<'a>> for Metadata {
fn from(metadata: &mpris::Metadata) -> Self { fn from(map: MetadataMap<'a>) -> Self {
let album = AlbumInfo { let album = AlbumInfo {
title: metadata.album_name().map(ToString::to_string), title: map
artists: metadata .get("xesam:album")
.album_artists() .and_then(|v| TryFrom::try_from(v).ok()),
.unwrap_or(Vec::new()) artists: map
.iter() .get("xesam:albumArtist")
.map(ToString::to_string) .cloned()
.collect(), .and_then(|v| TryFrom::try_from(v).ok())
.unwrap_or(Vec::new()),
}; };
let track = TrackInfo { let track = TrackInfo {
title: metadata.title().map(ToString::to_string), title: map
artists: metadata .get("xesam:title")
.artists() .and_then(|v| TryFrom::try_from(v).ok()),
.unwrap_or(Vec::new()) artists: map
.iter() .get("xesam:artist")
.map(ToString::to_string) .cloned()
.collect(), .and_then(|v| TryFrom::try_from(v).ok())
track_number: metadata.track_number(), .unwrap_or(Vec::new()),
track_number: map
.get("xesam:trackNumber")
.and_then(|v| TryFrom::try_from(v).ok()),
length: map
.get("xesam:length")
.and_then(|v| i64::try_from(v).ok())
.map(|us| us as f32 / 1_000_000.0), // 1,000,000 microseconds in a second
}; };
Self { album, track } Self { album, track }
} }
} }
impl MetadataTracker { impl Metadata {
pub fn new(magpie: &mut MagpieClient, metadata: &mpris::Metadata) -> Self { pub fn new(magpie: &mut MagpieClient, metadata: MetadataMap) -> Self {
let new: Self = metadata.into(); let new: Self = metadata.into();
magpie.send_panel_json(0, &InMsg::AlbumChanged(new.album.clone())); magpie.send_panel_json(0, &InMsg::AlbumChanged(new.album.clone()));
magpie.send_panel_json(0, &InMsg::TrackChanged(new.track.clone())); magpie.send_panel_json(0, &InMsg::TrackChanged(new.track.clone()));
magpie.send_panel_json(
0,
&InMsg::ProgressChanged(ProgressChanged {
position: 0.0,
length: metadata.length().map(|l| l.as_secs_f32()),
}),
);
new new
} }
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: &mpris::Metadata) { pub fn update(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap) {
let new: Self = metadata.into(); let new: Self = metadata.into();
if self.album != new.album { if self.album != new.album {
@ -65,19 +70,58 @@ impl MetadataTracker {
if self.track != new.track { if self.track != new.track {
messenger.send_panel_json(0, &InMsg::TrackChanged(new.track.clone())); messenger.send_panel_json(0, &InMsg::TrackChanged(new.track.clone()));
messenger.send_panel_json(
0,
&InMsg::ProgressChanged(ProgressChanged {
position: 0.0,
length: metadata.length().map(|l| l.as_secs_f32()),
}),
);
} }
*self = new; *self = new;
} }
} }
async fn player_main(
player: &PlayerProxy<'_>,
magpie: &mut MagpieClient<'_>,
) -> Result<(), Box<dyn std::error::Error>> {
use futures_util::StreamExt;
let mut playback_status = player.receive_playback_status_changed().await.fuse();
let mut metadata_tracker = player.receive_metadata_changed().await.fuse();
let mut metadata = Metadata::new(magpie, player.metadata().await?);
loop {
futures_util::select! {
// TODO also update volume, shuffle status, and loop status
status = playback_status.next() => {
let status = match status {
Some(v) => v,
None => break,
};
let status = status.get().await?;
let status = match status.as_str() {
"Playing" => Some(PlaybackStatus::Playing),
"Paused" => Some(PlaybackStatus::Paused),
"Stopped" => Some(PlaybackStatus::Stopped),
_ => None,
};
if let Some(status) = status {
magpie.send_panel_json(0, &InMsg::PlaybackStatusChanged(status));
}
}
new_metadata = metadata_tracker.next() => {
let new_metadata = match new_metadata {
Some(v) => v,
None => break,
};
let new_metadata = new_metadata.get().await?;
metadata.update(magpie, new_metadata);
}
};
}
Ok(())
}
fn main() { fn main() {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let module_path = args let module_path = args
@ -85,8 +129,6 @@ fn main() {
.expect("Please pass a path to a Canary script!") .expect("Please pass a path to a Canary script!")
.to_owned(); .to_owned();
let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus");
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
let sock_dir = Path::new(&sock_dir); let sock_dir = Path::new(&sock_dir);
let sock_path = sock_dir.join(MAGPIE_SOCK); let sock_path = sock_dir.join(MAGPIE_SOCK);
@ -103,96 +145,55 @@ fn main() {
let msg = MagpieServerMsg::CreatePanel(msg); let msg = MagpieServerMsg::CreatePanel(msg);
magpie.send(&msg).unwrap(); magpie.send(&msg).unwrap();
let mut first_loop = true; smol::block_on(async {
let mut connected = false; let dbus = zbus::Connection::session().await.unwrap();
loop { let mut first_loop = true;
if !first_loop { let mut connected = false;
let wait = std::time::Duration::from_secs(1);
std::thread::sleep(wait);
}
first_loop = false;
if connected {
println!("Disconnected from MPRIS");
let msg = InMsg::Disconnected;
magpie.send_panel_json(0, &msg);
connected = false;
}
println!("Connecting to MPRIS...");
let player = match player_finder.find_active() {
Ok(player) => player,
Err(err) => {
eprintln!("Couldn't find player: {:?}", err);
continue;
}
};
println!(
"Connected to \"{}\" ({})",
player.identity(),
player.bus_name()
);
connected = true;
magpie.send_panel_json(0, &InMsg::Connected);
let metadata = player.get_metadata().unwrap();
let mut metadata_tracker = MetadataTracker::new(&mut magpie, &metadata);
let mut events = match player.events() {
Ok(events) => events,
Err(err) => {
eprintln!("Player events D-Bus error: {:?}", err);
continue;
}
};
loop { loop {
let event = match events.next() { if !first_loop {
None => break, let wait = std::time::Duration::from_secs(1);
Some(Ok(e)) => e, std::thread::sleep(wait);
Some(Err(err)) => { }
eprintln!("D-Bus error while reading player events: {:?}", err);
first_loop = false;
if connected {
println!("Disconnected from MPRIS");
let msg = InMsg::Disconnected;
magpie.send_panel_json(0, &msg);
connected = false;
}
println!("Connecting to MPRIS...");
let player = match find_player(&dbus).await {
Ok(Some(player)) => player,
Ok(None) => {
eprintln!("Couldn't find player");
continue; continue;
} }
}; Err(err) => {
eprintln!("D-Bus error while finding player: {:?}", err);
use mpris::Event::*; return;
let in_msg = match event {
Playing => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Playing)),
Paused => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Paused)),
Stopped => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Stopped)),
LoopingChanged(status) => {
use mpris::LoopStatus::*;
let status = match status {
None => LoopStatus::None,
Track => LoopStatus::Track,
Playlist => LoopStatus::Playlist,
};
Some(InMsg::LoopingChanged(status))
}
ShuffleToggled(shuffle) => Some(InMsg::ShuffleChanged { shuffle }),
VolumeChanged(volume) => Some(InMsg::VolumeChanged {
volume: volume as f32,
}),
PlayerShutDown => None,
TrackChanged(ref metadata) => {
metadata_tracker.update(&mut magpie, metadata);
None
}
_ => {
eprintln!("Unhandled MPRIS message: {:?}", event);
None
} }
}; };
if let Some(msg) = in_msg { println!(
magpie.send_panel_json(0, &msg); "Connected to \"{}\" ({})",
player.path().as_str(),
player.destination().as_str()
);
connected = true;
magpie.send_panel_json(0, &InMsg::Connected);
match player_main(&player, &mut magpie).await {
Ok(()) => {}
Err(err) => {
eprintln!("D-Bus error while connected to player: {:?}", err);
}
} }
} }
} });
} }

View File

@ -0,0 +1,47 @@
use std::collections::HashMap;
use zbus::fdo::DBusProxy;
use zbus::zvariant::Value;
use zbus::{dbus_proxy, Connection, Result};
pub type MetadataMap<'a> = HashMap<String, Value<'a>>;
#[dbus_proxy(
interface = "org.mpris.MediaPlayer2.Player",
default_path = "/org/mpris/MediaPlayer2"
)]
trait Player {
fn next(&self) -> Result<()>;
fn previous(&self) -> Result<()>;
fn pause(&self) -> Result<()>;
fn play_pause(&self) -> Result<()>;
fn stop(&self) -> Result<()>;
fn play(&self) -> Result<()>;
#[dbus_proxy(property)]
fn playback_status(&self) -> Result<String>;
#[dbus_proxy(property)]
fn position(&self) -> Result<i64>;
#[dbus_proxy(property)]
fn metadata(&self) -> Result<MetadataMap>;
}
pub async fn find_player(connection: &Connection) -> Result<Option<PlayerProxy>> {
let dbus = DBusProxy::new(connection).await?;
let names = dbus.list_names().await?;
for name in names {
let name = name.as_str().to_string();
if name.starts_with("org.mpris.MediaPlayer2") {
let player = PlayerProxy::builder(connection)
.destination(name)?
.build()
.await?;
return Ok(Some(player));
}
}
Ok(None)
}

View File

@ -33,6 +33,7 @@ struct App {
last_update: Instant, last_update: Instant,
protocol_buf: String, protocol_buf: String,
bind_message_buf: String, bind_message_buf: String,
panel_bg: egui::Color32,
} }
impl App { impl App {
@ -49,6 +50,7 @@ impl App {
last_update: Instant::now(), last_update: Instant::now(),
protocol_buf: String::new(), protocol_buf: String::new(),
bind_message_buf: String::new(), bind_message_buf: String::new(),
panel_bg: egui::Color32::TRANSPARENT,
} }
} }
} }
@ -58,6 +60,8 @@ impl eframe::App for App {
ctx.request_repaint(); ctx.request_repaint();
egui::SidePanel::left("left_panel").show(ctx, |ui| { egui::SidePanel::left("left_panel").show(ctx, |ui| {
ui.heading("New Panel");
ui.label("Protocol name:"); ui.label("Protocol name:");
ui.text_edit_singleline(&mut self.protocol_buf); ui.text_edit_singleline(&mut self.protocol_buf);
@ -81,6 +85,14 @@ impl eframe::App for App {
self.panels.push(panel); self.panels.push(panel);
} }
ui.separator();
ui.heading("Global Settings");
ui.horizontal(|ui| {
ui.label("Panel background color: ");
ui.color_edit_button_srgba(&mut self.panel_bg);
});
}); });
let dt = self.last_update.elapsed().as_secs_f32(); let dt = self.last_update.elapsed().as_secs_f32();
@ -88,7 +100,7 @@ impl eframe::App for App {
for panel in self.panels.iter_mut() { for panel in self.panels.iter_mut() {
panel.panel.update(dt); panel.panel.update(dt);
panel.show(ctx); panel.show(self.panel_bg, ctx);
} }
} }
} }
@ -102,83 +114,87 @@ pub struct PanelWindow {
} }
impl PanelWindow { impl PanelWindow {
pub fn show(&mut self, ctx: &egui::Context) { pub fn show(&mut self, bg: egui::Color32, ctx: &egui::Context) {
let frame = egui::Frame::window(&ctx.style()).fill(bg);
let window_id = egui::Id::new(format!("panel_{}", self.index)); let window_id = egui::Id::new(format!("panel_{}", self.index));
egui::Window::new("Panel").id(window_id).show(ctx, |ui| { egui::Window::new("Panel")
egui::menu::bar(ui, |ui| { .frame(frame)
ui.checkbox(&mut self.show_msg, "Show Message Editor"); .id(window_id)
}); .show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.checkbox(&mut self.show_msg, "Show Message Editor");
});
let sense = egui::Sense { let sense = egui::Sense {
click: true, click: true,
drag: true, drag: true,
focusable: true, focusable: true,
};
let desired_size = ui.available_size();
let response = ui.allocate_response(desired_size, sense);
let rect = response.rect;
if rect.size() != self.current_size {
let size = rect.size();
self.current_size = size;
let size = canary::Vec2::new(size.x, size.y);
self.panel.on_resize(size * PX_PER_MM);
}
if let Some(hover_pos) = response.hover_pos() {
let local = (hover_pos - rect.left_top()) * PX_PER_MM;
let pos = canary::Vec2::new(local.x, local.y);
let kind = if response.drag_started() {
CursorEventKind::Select
} else if response.drag_released() {
CursorEventKind::Deselect
} else if response.dragged() {
CursorEventKind::Drag
} else {
CursorEventKind::Hover
}; };
self.panel.on_cursor_event(kind, pos); let desired_size = ui.available_size();
} let response = ui.allocate_response(desired_size, sense);
let rect = response.rect;
let texture = egui::TextureId::Managed(0); if rect.size() != self.current_size {
let uv = egui::pos2(0.0, 0.0); let size = rect.size();
let mut mesh = egui::Mesh::with_texture(texture); self.current_size = size;
let commands = self.panel.draw(); let size = canary::Vec2::new(size.x, size.y);
for command in commands.into_iter() { self.panel.on_resize(size * PX_PER_MM);
let voff = mesh.vertices.len() as u32;
match command {
canary::DrawCommand::Mesh { vertices, indices } => {
for v in vertices.iter() {
use egui::epaint::Vertex;
let pos = v.position / PX_PER_MM;
let pos = egui::pos2(pos.x, pos.y);
let pos = pos + rect.left_top().to_vec2();
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
let v = Vertex { pos, uv, color };
mesh.vertices.push(v);
}
for i in indices.iter() {
mesh.indices.push(i + voff);
}
}
_ => unimplemented!(),
} }
}
let painter = ui.painter_at(rect); if let Some(hover_pos) = response.hover_pos() {
let shape = egui::Shape::mesh(mesh); let local = (hover_pos - rect.left_top()) * PX_PER_MM;
painter.add(shape); let pos = canary::Vec2::new(local.x, local.y);
response let kind = if response.drag_started() {
}); CursorEventKind::Select
} else if response.drag_released() {
CursorEventKind::Deselect
} else if response.dragged() {
CursorEventKind::Drag
} else {
CursorEventKind::Hover
};
self.panel.on_cursor_event(kind, pos);
}
let texture = egui::TextureId::Managed(0);
let uv = egui::pos2(0.0, 0.0);
let mut mesh = egui::Mesh::with_texture(texture);
let commands = self.panel.draw();
for command in commands.into_iter() {
let voff = mesh.vertices.len() as u32;
match command {
canary::DrawCommand::Mesh { vertices, indices } => {
for v in vertices.iter() {
use egui::epaint::Vertex;
let pos = v.position / PX_PER_MM;
let pos = egui::pos2(pos.x, pos.y);
let pos = pos + rect.left_top().to_vec2();
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
let v = Vertex { pos, uv, color };
mesh.vertices.push(v);
}
for i in indices.iter() {
mesh.indices.push(i + voff);
}
}
_ => unimplemented!(),
}
}
let painter = ui.painter_at(rect);
let shape = egui::Shape::mesh(mesh);
painter.add(shape);
response
});
let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index)); let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index));
egui::Window::new("Message Editor") egui::Window::new("Message Editor")

View File

@ -1,4 +1,38 @@
#!/bin/sh #!/bin/sh
# Depends on: `rg` (ripgrep)
! rg --multiline --files-without-match --glob '*.rs' --pcre2 '(?<!\n)((//)|(#)) Copyright \(c\) \d+ [A-z, ]+\n((//)|(#)) SPDX-License-Identifier: .*\n' set -e
# check if we have tomcat(1)
if ! command -v tomcat >/dev/null 2>&1; then
printf "%s: Missing dependency: tomcat(1)\n"
exit 69 # sysexits(3) EX_UNAVAILABLE
fi
dir="$(pwd | sed 's/\//\n/g' | tail -n 1)"
for toml in $(find "$PWD" -name "Cargo.toml"); do
printf "Project: %s\n" "$(tomcat package.name "$toml")"
for file in $(find "$(printf "%s\n" "$toml" |\
sed 's/Cargo\.toml/src/g')" -name "*.rs")
do
info="$(head -n 2 "$file")"
toml_lic="$(tomcat package.license "$toml")"
if ! test -n "$toml_lic"; then
printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\
sed "s/^.\+$dir\///g")"
continue 2
fi
if ! [ "$toml_lic" = "$(printf "%s\n" "$info" | tail -n 1 |\
sed -n 's/\/\/ SPDX-License-Identifier: //p')" ]
then
printf "%s: Missing or malformed license information\n" \
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
fi
if ! test -n "$(printf "%s\n" "$info" | head -n 1 |\
sed -n '/\/\/ Copyright (c) .\+/p')"
then
printf "%s: Missing or malformed copyright holder information\n" \
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
fi
done
done

View File

@ -173,13 +173,13 @@ impl Color {
) )
} }
pub fn alpha_multiply(&self, mul: u8) -> Self { pub const fn alpha_multiply(&self, mul: u8) -> Self {
let a = self.0 as u8 as u16; let a = self.0 as u8 as u16;
let multiplied = ((a * (mul as u16)) >> 8) as u8; let multiplied = ((a * (mul as u16)) >> 8) as u8;
self.with_alpha(multiplied) self.with_alpha(multiplied)
} }
pub fn with_alpha(&self, alpha: u8) -> Self { pub const fn with_alpha(&self, alpha: u8) -> Self {
Self(self.0 & 0xffffff00 | alpha as u32) Self(self.0 & 0xffffff00 | alpha as u32)
} }

View File

@ -7,6 +7,7 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub mod anim; pub mod anim;
pub mod main_menu; pub mod main_menu;
pub mod music_player; pub mod music_player;
pub mod style;
pub mod widgets; pub mod widgets;
use api::*; use api::*;
@ -23,6 +24,7 @@ fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn Pan
match protocol.as_str() { match protocol.as_str() {
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg), "tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
"wip-dialog" => ConfirmationDialogPanel::bind(panel, msg),
_ => MainMenuPanel::bind(panel, msg), _ => MainMenuPanel::bind(panel, msg),
} }
} }
@ -50,15 +52,23 @@ impl PanelImpl for ConfirmationDialogPanel {
self.dialog.on_cursor_event(kind, at.into()); self.dialog.on_cursor_event(kind, at.into());
} }
fn on_resize(&mut self, _size: Vec2) {} fn on_resize(&mut self, size: Vec2) {
self.dialog.resize(size);
}
fn on_message(&mut self, _msg: Message) {} fn on_message(&mut self, _msg: Message) {}
} }
impl ConfirmationDialogPanel { impl ConfirmationDialogPanel {
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> { pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
let msg = msg.to_vec(); // let msg = msg.to_vec();
let info: DialogInfo = serde_json::from_slice(&msg).unwrap(); // let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
let info = DialogInfo {
title: "Hello world!".to_string(),
content: "Testing, testing...".to_string(),
responses: vec![DialogResponse::Yes, DialogResponse::No],
};
use widgets::dialog::*; use widgets::dialog::*;
let style = DialogStyle::default(); let style = DialogStyle::default();

View File

@ -7,6 +7,7 @@ use crate::{DrawContext, Rect};
use button::{RectButton, RoundButton, RoundButtonStyle}; use button::{RectButton, RoundButton, RoundButtonStyle};
use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle}; use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle};
use menu::{SlotMenu, SlotMenuEvent, TabMenu}; use menu::{SlotMenu, SlotMenuEvent, TabMenu};
use palette::Palette;
use shell::{Offset, OffsetAlignment, Popup, Reveal}; use shell::{Offset, OffsetAlignment, Popup, Reveal};
use text::LabelText; use text::LabelText;
@ -47,6 +48,7 @@ pub struct MainMenu {
pub menu: Offset<SlotMenu<RoundButton>>, pub menu: Offset<SlotMenu<RoundButton>>,
pub player_info: Reveal<Offset<PlayerInfo>>, pub player_info: Reveal<Offset<PlayerInfo>>,
pub inventory: Reveal<Offset<TabMenu>>, pub inventory: Reveal<Offset<TabMenu>>,
pub palette: Reveal<Offset<Palette>>,
pub settings: Reveal<Offset<SettingsMenu>>, pub settings: Reveal<Offset<SettingsMenu>>,
} }
@ -64,9 +66,9 @@ impl Default for MainMenu {
radius: 7.5, radius: 7.5,
spacing: 1.5, spacing: 1.5,
thickness: 0.4, thickness: 0.4,
body_color: Color::WHITE, body_color: THEME.palette.surface,
ring_color: Color::WHITE, ring_color: THEME.palette.surface,
icon_color: Color::BLACK, icon_color: THEME.palette.text,
}; };
let mut buttons = Vec::new(); let mut buttons = Vec::new();
@ -102,6 +104,15 @@ impl Default for MainMenu {
let inventory = Offset::new(inventory, submenu_spacing_right); let inventory = Offset::new(inventory, submenu_spacing_right);
let inventory = Reveal::new(inventory, reveal_slide, reveal_duration); let inventory = Reveal::new(inventory, reveal_slide, reveal_duration);
let palette = Palette::new(Default::default());
let palette = Offset::new_aligned(
palette,
submenu_spacing_left,
OffsetAlignment::End,
OffsetAlignment::Center,
);
let palette = Reveal::new(palette, -reveal_slide, reveal_duration);
let settings = SettingsMenu::new(); let settings = SettingsMenu::new();
let settings = Offset::new(settings, submenu_spacing_right); let settings = Offset::new(settings, submenu_spacing_right);
let settings = Reveal::new(settings, reveal_slide, reveal_duration); let settings = Reveal::new(settings, reveal_slide, reveal_duration);
@ -110,6 +121,7 @@ impl Default for MainMenu {
menu, menu,
player_info, player_info,
inventory, inventory,
palette,
settings, settings,
} }
} }
@ -120,6 +132,7 @@ impl Container for MainMenu {
f(&mut self.menu); f(&mut self.menu);
f(&mut self.player_info); f(&mut self.player_info);
f(&mut self.inventory); f(&mut self.inventory);
f(&mut self.palette);
f(&mut self.settings); f(&mut self.settings);
} }
@ -139,8 +152,14 @@ impl Container for MainMenu {
self.player_info.hide(); self.player_info.hide();
self.inventory.hide(); self.inventory.hide();
} }
SlotMenuEvent::SubmenuOpen(4) => self.settings.show(), SlotMenuEvent::SubmenuOpen(4) => {
SlotMenuEvent::SubmenuClose(4) => self.settings.hide(), self.palette.show();
self.settings.show();
}
SlotMenuEvent::SubmenuClose(4) => {
self.palette.hide();
self.settings.hide();
}
_ => {} _ => {}
}; };
} }
@ -150,6 +169,7 @@ pub struct PlayerInfo {
width: f32, width: f32,
height: f32, height: f32,
rounding: f32, rounding: f32,
color: Color,
} }
impl PlayerInfo { impl PlayerInfo {
@ -158,6 +178,7 @@ impl PlayerInfo {
width: 70.0, width: 70.0,
height: 120.0, height: 120.0,
rounding: 5.0, rounding: 5.0,
color: THEME.palette.surface,
} }
} }
} }
@ -170,7 +191,7 @@ impl RectBounds for PlayerInfo {
impl Widget for PlayerInfo { impl Widget for PlayerInfo {
fn draw(&mut self, ctx: &DrawContext) { fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, Color::WHITE); ctx.draw_rounded_rect(self.get_bounds(), self.rounding, self.color);
} }
} }

View File

@ -192,7 +192,8 @@ impl MusicPlayerWidget {
text: content.to_string(), text: content.to_string(),
}; };
let label = Label::new_centered(text, 10.0, Color::BLACK); let color = style.body.text_color;
let label = Label::new_centered(text, 10.0, color);
Offset::new(label, Vec2::ZERO) Offset::new(label, Vec2::ZERO)
}; };
@ -209,7 +210,7 @@ impl MusicPlayerWidget {
text, text,
HorizontalAlignment::Center, HorizontalAlignment::Center,
scale, scale,
Color::BLACK, THEME.palette.text,
0.0, 0.0,
0.0, 0.0,
baseline, baseline,
@ -238,18 +239,18 @@ impl MusicPlayerWidget {
radius: style.footer.height * 0.3, radius: style.footer.height * 0.3,
spacing: style.footer.height * 0.1, spacing: style.footer.height * 0.1,
thickness: style.footer.height * 0.025, thickness: style.footer.height * 0.025,
body_color: Color(0xf1b841ff), body_color: THEME.palette.yellow,
ring_color: Color(0xf1b841ff), ring_color: THEME.palette.yellow,
icon_color: Color::BLACK, icon_color: THEME.palette.black,
}; };
let secondary_button = RoundButtonStyle { let secondary_button = RoundButtonStyle {
radius: style.footer.height * 0.25, radius: style.footer.height * 0.25,
spacing: style.footer.height * 0.05, spacing: style.footer.height * 0.05,
thickness: style.footer.height * 0.025, thickness: style.footer.height * 0.025,
body_color: Color::WHITE, body_color: style.footer.color,
ring_color: Color::BLACK, ring_color: THEME.palette.black,
icon_color: Color::BLACK, icon_color: THEME.palette.black,
}; };
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text)); let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
@ -354,17 +355,17 @@ impl MusicPlayerWidget {
.map(|s| s.as_str()) .map(|s| s.as_str())
.unwrap_or("<album here>"), .unwrap_or("<album here>"),
); );
if let Some(length) = info.length {
self.duration.set_text(&Self::format_time(length));
} else {
self.duration.set_text("--:--");
}
} }
pub fn update_progress(&mut self, progress: ProgressChanged) { pub fn update_progress(&mut self, progress: ProgressChanged) {
self.position_secs = progress.position; self.position_secs = progress.position;
self.position_dirty = true; self.position_dirty = true;
if let Some(length) = progress.length {
self.duration.set_text(&Self::format_time(length));
} else {
self.duration.set_text("--:--");
}
} }
pub fn update_playback_status(&mut self, status: PlaybackStatus) { pub fn update_playback_status(&mut self, status: PlaybackStatus) {

170
scripts/sao-ui/src/style.rs Normal file
View File

@ -0,0 +1,170 @@
use canary_script::Color;
/// A reusable set of colors. Used by default widget styles.
pub struct Palette {
pub base: Color,
pub base_hover: Color,
pub base_active: Color,
pub surface: Color,
pub overlay: Color,
pub text: Color,
pub black: Color,
pub red: Color,
pub green: Color,
pub yellow: Color,
pub blue: Color,
pub magenta: Color,
pub cyan: Color,
pub white: Color,
}
impl Palette {
pub fn make_label_pairs(&self) -> Vec<(&'static str, Color)> {
vec![
("Base", self.base),
("Base Hover", self.base_hover),
("Base Active", self.base_active),
("Surface", self.surface),
("Overlay", self.overlay),
("Text", self.text),
("Black", self.black),
("Red", self.red),
("Green", self.green),
("Yellow", self.yellow),
("Blue", self.blue),
("Magenta", self.magenta),
("Cyan", self.cyan),
("White", self.white),
]
}
}
/// The common base color alpha shared between all themes.
pub const BASE_ALPHA: u8 = 0xc0;
/// The common base_hover color alpha shared between all themes.
pub const BASE_HOVER_ALPHA: u8 = 0xe0;
/// Converts 0xrrggbb hex to an opaque [Color].
pub const fn hex(rgb: u32) -> Color {
Color((rgb << 8) | 0xff)
}
/// Sword Art Online color palette.
pub const SAO_PALETTE: Palette = Palette {
base: Color::WHITE.with_alpha(BASE_ALPHA),
base_hover: Color::WHITE.with_alpha(BASE_HOVER_ALPHA),
base_active: Color::YELLOW,
surface: Color::WHITE,
overlay: Color::WHITE,
text: Color::BLACK,
black: Color::BLACK,
red: Color::RED,
green: Color::GREEN,
yellow: Color::YELLOW,
blue: Color::BLUE,
magenta: Color::MAGENTA,
cyan: Color::CYAN,
white: Color::WHITE,
};
/// Rose Pine color palette.
pub const ROSE_PINE_PALETTE: Palette = Palette {
base: hex(0x191724).with_alpha(BASE_ALPHA),
base_hover: hex(0x21202ee0).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
base_active: hex(0x403d52), // Highlight Med
surface: hex(0x1f1d2e),
overlay: hex(0x26233a),
text: hex(0xe0def4),
black: hex(0x6e6a86), // Muted
red: hex(0xeb6f92), // Love
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
yellow: hex(0xf6c177), // Gold
blue: hex(0x31748f), // Pine
magenta: hex(0xc4a7e7), // Iris
cyan: hex(0x9ccfd8), // Foam
white: hex(0xe0def4), // Text
};
/// Rose Pine Moon color palette.
pub const ROSE_PINE_MOON_PALETTE: Palette = Palette {
base: hex(0x232136).with_alpha(BASE_ALPHA),
base_hover: hex(0x2a283e).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
base_active: hex(0x44415a), // Highlight Med
surface: hex(0x2a273f),
overlay: hex(0x393552),
text: hex(0xe0def4),
black: hex(0x6e6a86), // Muted
red: hex(0xeb6f92), // Love
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
yellow: hex(0xf6c177), // Gold
blue: hex(0x3e8fb0), // Pine
magenta: hex(0xc4a7e7), // Iris
cyan: hex(0x9ccfd8), // Foam
white: hex(0xe0def4), // Text
};
/// [Arctica](https://github.com/sashakoshka/arctica) indexable color theme.
pub const ARCTICA: [Color; 24] = [
hex(0x242933),
hex(0x2e3440),
hex(0x3b4252),
hex(0x4c566a),
hex(0xeceff4),
hex(0xd8dee9),
hex(0xc2c9d6),
hex(0xaeb7c6),
hex(0xa8555d),
hex(0xb77763),
hex(0xcdb179),
hex(0x8ba277),
hex(0x769b9b),
hex(0x72a1ae),
hex(0x5e81ac),
hex(0x92738c),
hex(0xbf616a),
hex(0xd08770),
hex(0xebcb8b),
hex(0xa3be8c),
hex(0x8fbcbb),
hex(0x88c0d0),
hex(0x81a1c1),
hex(0xb48ead),
];
/// [Arctica](https://github.com/sashakoshka/arctica) color palette.
pub const ARCTICA_PALETTE: Palette = Palette {
base: ARCTICA[0].with_alpha(BASE_ALPHA),
base_hover: ARCTICA[1].with_alpha(BASE_HOVER_ALPHA),
base_active: ARCTICA[13],
surface: ARCTICA[2],
overlay: ARCTICA[3],
text: ARCTICA[5],
black: ARCTICA[3],
red: ARCTICA[8],
green: ARCTICA[11],
yellow: ARCTICA[10],
blue: ARCTICA[14],
magenta: ARCTICA[15],
cyan: ARCTICA[13],
white: ARCTICA[7],
};
/// Common measurements for widget shapes.
pub struct Metrics {
pub surface_rounding: f32,
}
/// Common default parameters for widget styles.
pub struct Theme {
pub palette: Palette,
pub metrics: Metrics,
}
/// The global theme.
pub const THEME: Theme = Theme {
palette: ARCTICA_PALETTE,
metrics: Metrics {
surface_rounding: 5.0,
},
};

View File

@ -118,6 +118,8 @@ pub struct RectButtonStyle {
pub inactive_color: Color, pub inactive_color: Color,
pub hover_color: Color, pub hover_color: Color,
pub selected_color: Color, pub selected_color: Color,
pub icon_color: Color,
pub label_color: Color,
} }
impl Default for RectButtonStyle { impl Default for RectButtonStyle {
@ -129,9 +131,11 @@ impl Default for RectButtonStyle {
label_baseline: 0.25, label_baseline: 0.25,
icon_scale_factor: 0.8, icon_scale_factor: 0.8,
icon_margin_factor: 1.1, icon_margin_factor: 1.1,
inactive_color: Color::WHITE.with_alpha(0x40), inactive_color: THEME.palette.base,
hover_color: Color::WHITE.with_alpha(0xb0), hover_color: THEME.palette.base_hover,
selected_color: Color::YELLOW, selected_color: THEME.palette.base_active,
icon_color: THEME.palette.black,
label_color: THEME.palette.text,
} }
} }
} }
@ -168,7 +172,7 @@ impl RectButton {
label_left += margin; label_left += margin;
alignment = HorizontalAlignment::Left; alignment = HorizontalAlignment::Left;
let scale = rect.height() * style.icon_scale_factor; let scale = rect.height() * style.icon_scale_factor;
let color = Color::BLACK; let color = style.icon_color;
let cx = rect.tl.x + margin / 2.0; let cx = rect.tl.x + margin / 2.0;
let cy = rect.tl.y + rect.height() / 2.0; let cy = rect.tl.y + rect.height() / 2.0;
let center = Vec2::new(cx, cy); let center = Vec2::new(cx, cy);
@ -182,7 +186,7 @@ impl RectButton {
let right = rect.br.x; let right = rect.br.x;
let baseline = rect.tl.y; let baseline = rect.tl.y;
let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline; let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline;
let color = Color::BLACK; let color = style.label_color;
Label::new(text, alignment, scale, color, left, right, baseline) Label::new(text, alignment, scale, color, left, right, baseline)
}); });

View File

@ -23,8 +23,8 @@ impl DialogResponse {
pub fn get_color(&self) -> Color { pub fn get_color(&self) -> Color {
match self { match self {
DialogResponse::Yes => Color::BLUE, DialogResponse::Yes => THEME.palette.blue,
DialogResponse::No => Color::RED, DialogResponse::No => THEME.palette.red,
} }
} }
} }
@ -40,7 +40,7 @@ pub struct DialogStyle {
impl Default for DialogStyle { impl Default for DialogStyle {
fn default() -> Self { fn default() -> Self {
Self { Self {
rounding: 5.0, rounding: THEME.metrics.surface_rounding,
header: Default::default(), header: Default::default(),
body: Default::default(), body: Default::default(),
footer: Default::default(), footer: Default::default(),
@ -61,10 +61,10 @@ pub struct DialogHeaderStyle {
impl Default for DialogHeaderStyle { impl Default for DialogHeaderStyle {
fn default() -> Self { fn default() -> Self {
Self { Self {
color: Color::WHITE, color: THEME.palette.surface,
height: 20.0, height: 20.0,
text_font: Font::new(crate::DISPLAY_FONT), text_font: Font::new(crate::DISPLAY_FONT),
text_color: Color::BLACK, text_color: THEME.palette.text,
text_scale_factor: 0.65, text_scale_factor: 0.65,
text_baseline: 0.25, text_baseline: 0.25,
} }
@ -82,9 +82,9 @@ pub struct DialogBodyStyle {
impl Default for DialogBodyStyle { impl Default for DialogBodyStyle {
fn default() -> Self { fn default() -> Self {
Self { Self {
color: Color::WHITE.with_alpha(0xb0), color: THEME.palette.base,
text_font: Font::new(crate::CONTENT_FONT), text_font: Font::new(crate::CONTENT_FONT),
text_color: Color::BLACK, text_color: THEME.palette.text,
text_size: 5.0, text_size: 5.0,
} }
} }
@ -95,6 +95,7 @@ pub struct DialogFooterStyle {
pub icon_font: Font, pub icon_font: Font,
pub button_radius: f32, pub button_radius: f32,
pub color: Color, pub color: Color,
pub button_fg: Color,
pub height: f32, pub height: f32,
} }
@ -103,7 +104,8 @@ impl Default for DialogFooterStyle {
Self { Self {
icon_font: Font::new(crate::ICON_FONT), icon_font: Font::new(crate::ICON_FONT),
button_radius: 7.5, button_radius: 7.5,
color: Color::WHITE, color: THEME.palette.surface,
button_fg: THEME.palette.white,
height: 15.0, height: 15.0,
} }
} }
@ -136,7 +138,7 @@ impl Dialog {
thickness: radius * 0.05, thickness: radius * 0.05,
body_color: color, body_color: color,
ring_color: color, ring_color: color,
icon_color: Color::WHITE, icon_color: style.footer.button_fg,
}; };
let text = LabelText { let text = LabelText {

View File

@ -224,6 +224,7 @@ pub struct TabMenu {
impl TabMenu { impl TabMenu {
const HEAD_RADIUS: f32 = 5.0; const HEAD_RADIUS: f32 = 5.0;
const HEAD_HEIGHT: f32 = 15.0; const HEAD_HEIGHT: f32 = 15.0;
const HEAD_COLOR: Color = THEME.palette.surface;
const TAB_WIDTH: f32 = 15.0; const TAB_WIDTH: f32 = 15.0;
const TAB_HEIGHT: f32 = 25.0; const TAB_HEIGHT: f32 = 25.0;
const TAB_NUM: usize = 6; const TAB_NUM: usize = 6;
@ -235,9 +236,9 @@ impl TabMenu {
radius: Self::HEAD_HEIGHT * 0.25, radius: Self::HEAD_HEIGHT * 0.25,
spacing: Self::HEAD_HEIGHT * 0.1, spacing: Self::HEAD_HEIGHT * 0.1,
thickness: Self::HEAD_HEIGHT * 0.05, thickness: Self::HEAD_HEIGHT * 0.05,
body_color: Color::WHITE, body_color: Self::HEAD_COLOR,
ring_color: Color::BLACK, ring_color: THEME.palette.black,
icon_color: Color::BLACK, icon_color: THEME.palette.black,
}; };
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0; const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
@ -348,20 +349,18 @@ impl Container for TabMenu {
} }
fn draw(&mut self, ctx: &DrawContext) { fn draw(&mut self, ctx: &DrawContext) {
let head_color = Color::WHITE;
ctx.draw_partially_rounded_rect( ctx.draw_partially_rounded_rect(
CornerFlags::BOTTOM_RIGHT, CornerFlags::BOTTOM_RIGHT,
self.separator_rect, self.separator_rect,
Self::INNER_RADIUS, Self::INNER_RADIUS,
head_color, Self::HEAD_COLOR,
); );
ctx.draw_partially_rounded_rect( ctx.draw_partially_rounded_rect(
CornerFlags::TOP, CornerFlags::TOP,
self.head_rect, self.head_rect,
Self::HEAD_RADIUS, Self::HEAD_RADIUS,
head_color, Self::HEAD_COLOR,
); );
} }
} }

View File

@ -8,6 +8,7 @@ pub mod button;
pub mod dialog; pub mod dialog;
pub mod flex; pub mod flex;
pub mod menu; pub mod menu;
pub mod palette;
pub mod scroll; pub mod scroll;
pub mod shell; pub mod shell;
pub mod text; pub mod text;
@ -75,6 +76,7 @@ impl<T: Container> Widget for T {
pub mod prelude { pub mod prelude {
pub use super::*; pub use super::*;
pub use crate::anim::Animation; pub use crate::anim::Animation;
pub use crate::style::{self, THEME};
pub use canary_script::{*, api::*}; pub use canary_script::{*, api::*};
pub use keyframe::functions::*; pub use keyframe::functions::*;
} }

View File

@ -0,0 +1,106 @@
use super::prelude::*;
use shell::Offset;
use text::{HorizontalAlignment, Label, LabelText};
pub struct PaletteStyle {
pub bg: Color,
pub text: Color,
pub rounding: f32,
pub text_size: f32,
pub line_spacing: f32,
pub color_radius: f32,
pub margin: Rect,
}
impl Default for PaletteStyle {
fn default() -> Self {
Self {
bg: THEME.palette.surface,
text: THEME.palette.text,
rounding: THEME.metrics.surface_rounding,
text_size: 5.0,
line_spacing: 8.0,
color_radius: 3.0,
margin: Rect::from_xy_size(Vec2::splat(10.0), Vec2::ZERO),
}
}
}
/// A widget that displays all the colors in the global palette.
pub struct Palette {
body: Rect,
style: PaletteStyle,
labels: Vec<Offset<Label>>,
colors: Vec<(Vec2, Color)>,
}
impl Palette {
pub fn new(style: PaletteStyle) -> Self {
let width = 70.0;
let pairs = THEME.palette.make_label_pairs();
let label_font = Font::new(crate::CONTENT_FONT);
let mut label_cursor = Vec2::new(0.0, style.line_spacing) + style.margin.tl;
let mut color_cursor = Vec2::new(
width - style.margin.br.x,
style.line_spacing / 2.0 + style.margin.tl.y,
);
let mut labels = Vec::new();
let mut colors = Vec::new();
for (text, color) in pairs {
let text = LabelText {
font: label_font,
text: text.to_string(),
};
let label = Label::new(
text,
HorizontalAlignment::Left,
style.text_size,
style.text,
0.0,
0.0,
0.0,
);
let label = Offset::new(label, label_cursor);
labels.push(label);
colors.push((color_cursor, color));
label_cursor.y += style.line_spacing;
color_cursor.y += style.line_spacing;
}
let height = label_cursor.y + style.margin.br.y;
Self {
body: Rect::from_xy_size(Vec2::ZERO, Vec2::new(width, height)),
style,
labels,
colors,
}
}
}
impl RectBounds for Palette {
fn get_bounds(&self) -> Rect {
self.body
}
}
impl Widget for Palette {
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_rounded_rect(self.body, self.style.rounding, self.style.bg);
for label in self.labels.iter_mut() {
label.draw(ctx);
}
for (center, color) in self.colors.iter() {
ctx.draw_circle(*center, self.style.color_radius, *color);
}
}
}

View File

@ -21,11 +21,11 @@ impl Default for ScrollBarStyle {
margin: Vec2::splat(2.0), margin: Vec2::splat(2.0),
body_radius: 1.0, body_radius: 1.0,
body_width: 3.0, body_width: 3.0,
body_idle_color: Color(0x7f7f7fff), body_idle_color: THEME.palette.base,
body_hover_color: Color(0xb0b0b0ff), body_hover_color: THEME.palette.base_hover,
body_selected_color: Color::WHITE, body_selected_color: THEME.palette.base_active,
rail_width: 1.0, rail_width: 1.0,
rail_color: Color(0xa0a0a07f), rail_color: THEME.palette.base,
} }
} }
} }