use std::collections::HashMap;
use std::path::{ Path, PathBuf };
use serde::{ Deserialize, Deserializer, Serializer };
use serde::de::Error as DeError;
use crate::types::Color;
use super::document::{ Mode, ThemeDocument };
use super::fonts::{ FontFamilyDef, FontSource };
use super::palette::default_window_controls;
use super::slots::{ Slot, SlotStore };
use super::
{
LauncherSpec, ThemeError, ThemeMode, WallpaperSpec,
WindowControlsSpec,
};
mod raw;
mod refs;
#[ cfg( test ) ]
mod tests;
use raw::*;
use refs::*;
pub mod color_serde
{
use super::*;
pub fn parse( s: &str ) -> Result<Color, String>
{
let t = s.trim();
if t.starts_with( "rgba(" ) || t.starts_with( "rgb(" )
{
parse_functional( t )
}
else
{
parse_hex( t )
}
}
fn parse_hex( s: &str ) -> Result<Color, String>
{
let h = s.trim_start_matches( '#' );
let ( r, g, b, a ) = match h.len()
{
6 =>
{
let r = u8::from_str_radix( &h[0..2], 16 ).map_err( |_| bad( s ) )?;
let g = u8::from_str_radix( &h[2..4], 16 ).map_err( |_| bad( s ) )?;
let b = u8::from_str_radix( &h[4..6], 16 ).map_err( |_| bad( s ) )?;
( r, g, b, 0xFF )
}
8 =>
{
let r = u8::from_str_radix( &h[0..2], 16 ).map_err( |_| bad( s ) )?;
let g = u8::from_str_radix( &h[2..4], 16 ).map_err( |_| bad( s ) )?;
let b = u8::from_str_radix( &h[4..6], 16 ).map_err( |_| bad( s ) )?;
let a = u8::from_str_radix( &h[6..8], 16 ).map_err( |_| bad( s ) )?;
( r, g, b, a )
}
_ => return Err( bad( s ) ),
};
Ok( Color
{
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: a as f32 / 255.0,
})
}
fn parse_functional( s: &str ) -> Result<Color, String>
{
let ( with_alpha, inner ) = if let Some( rest ) = s.strip_prefix( "rgba(" )
{
( true, rest.strip_suffix( ')' ).ok_or_else( || bad( s ) )? )
}
else if let Some( rest ) = s.strip_prefix( "rgb(" )
{
( false, rest.strip_suffix( ')' ).ok_or_else( || bad( s ) )? )
}
else
{
return Err( bad( s ) );
};
let parts: Vec<&str> = inner.split( ',' ).map( str::trim ).collect();
let expected = if with_alpha { 4 } else { 3 };
if parts.len() != expected { return Err( bad( s ) ); }
let r = parse_channel( parts[0] ).ok_or_else( || bad( s ) )?;
let g = parse_channel( parts[1] ).ok_or_else( || bad( s ) )?;
let b = parse_channel( parts[2] ).ok_or_else( || bad( s ) )?;
let a = if with_alpha
{
parts[3].parse::<f32>().map_err( |_| bad( s ) )?.clamp( 0.0, 1.0 )
}
else
{
1.0
};
Ok( Color { r: r / 255.0, g: g / 255.0, b: b / 255.0, a })
}
fn parse_channel( s: &str ) -> Option<f32>
{
if let Ok( n ) = s.parse::<u16>()
{
if n <= 255 { return Some( n as f32 ); }
return None;
}
if let Ok( f ) = s.parse::<f32>()
{
if ( 0.0..=255.0 ).contains( &f ) { return Some( f ); }
}
None
}
fn bad( s: &str ) -> String
{
format!
(
"invalid colour `{}` (expected `#RRGGBB`, `#RRGGBBAA` or `rgb[a](…)`)",
s
)
}
pub fn format( c: Color ) -> String
{
let r = (c.r.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
let g = (c.g.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
let b = (c.b.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
let a = (c.a.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
if a == 0xFF
{
format!( "#{:02X}{:02X}{:02X}", r, g, b )
}
else
{
format!( "#{:02X}{:02X}{:02X}{:02X}", r, g, b, a )
}
}
pub fn serialize<S>( c: &Color, ser: S ) -> Result<S::Ok, S::Error>
where S: Serializer
{
ser.serialize_str( &format( *c ) )
}
pub fn deserialize<'de, D>( de: D ) -> Result<Color, D::Error>
where D: Deserializer<'de>
{
let s = String::deserialize( de )?;
parse( &s ).map_err( D::Error::custom )
}
}
pub fn parse_color_str( s: &str ) -> Result<Color, String>
{
color_serde::parse( s )
}
fn family_from_raw( root: Option<&Path>, r: RawFontFamily ) -> FontFamilyDef
{
FontFamilyDef
{
name: r.name,
fallbacks: r.fallbacks,
sources: r.sources.into_iter().map( |s| FontSource
{
weight: s.weight,
style: s.style.into(),
path: resolve_relative( root, &s.path ),
}).collect(),
}
}
fn wallpaper_from_raw( root: Option<&Path>, r: RawWallpaper ) -> WallpaperSpec
{
WallpaperSpec { path: Some( resolve_relative( root, &r.path ) ), fit: r.fit }
}
fn window_controls_from_raw
(
fallback_palette: Option<&super::Palette>,
_mode: ThemeMode,
raw: RawWindowControls,
) -> Result<WindowControlsSpec, ThemeError>
{
let fallback = match fallback_palette
{
Some( p ) => default_window_controls( *p ),
None => WindowControlsSpec
{
bar_bg: Color::WHITE,
icon: Color::BLACK,
hover_bg: Color::rgba( 0.0, 0.0, 0.0, 0.08 ),
pressed_bg: Color::rgba( 0.0, 0.0, 0.0, 0.12 ),
close_hover_bg: Color::rgba( 0.92, 0.18, 0.18, 0.90 ),
close_icon: Color::WHITE,
focus_ring: Color::hex( 0x04, 0xD9, 0xFE ),
},
};
Ok( WindowControlsSpec
{
bar_bg: parse_opt( raw.bar_bg.as_deref(), fallback.bar_bg )?,
icon: parse_opt( raw.icon.as_deref(), fallback.icon )?,
hover_bg: parse_opt( raw.hover_bg.as_deref(), fallback.hover_bg )?,
pressed_bg: parse_opt( raw.pressed_bg.as_deref(), fallback.pressed_bg )?,
close_hover_bg: parse_opt( raw.close_hover_bg.as_deref(), fallback.close_hover_bg )?,
close_icon: parse_opt( raw.close_icon.as_deref(), fallback.close_icon )?,
focus_ring: parse_opt( raw.focus_ring.as_deref(), fallback.focus_ring )?,
})
}
fn parse_opt( s: Option<&str>, fallback: Color ) -> Result<Color, ThemeError>
{
match s
{
Some( v ) => parse_color_str( v ).map_err( ThemeError::InvalidColor ),
None => Ok( fallback ),
}
}
fn mode_from_raw( root: Option<&Path>, r: RawMode ) -> Result<Mode, ThemeError>
{
let mut store = SlotStore::new();
for ( id, raw ) in r.slots
{
store.insert( id, Slot::from( raw ) );
}
let wallpaper = r.wallpaper.map( |w| wallpaper_from_raw( root, w ) );
let lockscreen = r.lockscreen.map( |w| wallpaper_from_raw( root, w ) );
let launcher = match r.launcher
{
Some( l ) => Some( LauncherSpec
{
background: parse_color_str( &l.background ).map_err( ThemeError::InvalidColor )?,
border_radius: l.border_radius,
}),
None => None,
};
let window_controls = match r.window_controls
{
Some( raw ) => Some( window_controls_from_raw( None, ThemeMode::Light, raw )? ),
None => None,
};
Ok( Mode { wallpaper, lockscreen, launcher, window_controls, slots: store } )
}
pub fn parse_document_json( text: &str, root: Option<&Path> )
-> Result<ThemeDocument, ThemeError>
{
let mut value: serde_json::Value = serde_json::from_str( text ).map_err( |e|
ThemeError::ParseJson( root.map( Path::to_path_buf ).unwrap_or_default(), e )
)?;
let colors = extract_colors_map( &value )?;
let gradients = extract_gradients_map( &value )?;
let inset_stacks = extract_inset_stacks_map( &value )?;
let mut tokens = gradients;
for ( k, v ) in inset_stacks
{
if tokens.contains_key( &k )
{
return Err( ThemeError::InvalidColor( format!(
"name `{}` is defined in both `gradients` and `inset_stacks`", k
)));
}
tokens.insert( k, v );
}
let empty_tokens = HashMap::new();
for ( _, t ) in tokens.iter_mut()
{
resolve_refs( t, &colors, &empty_tokens )?;
}
if let serde_json::Value::Object( ref mut map ) = value
{
map.remove( "colors" );
map.remove( "gradients" );
map.remove( "inset_stacks" );
}
resolve_refs( &mut value, &colors, &tokens )?;
let raw: RawThemeDocument = serde_json::from_value( value ).map_err( |e|
ThemeError::ParseJson( root.map( Path::to_path_buf ).unwrap_or_default(), e )
)?;
let fonts = raw.fonts
.into_iter()
.map( |( k, v )| ( k, family_from_raw( root, v ) ) )
.collect();
let light = mode_from_raw( root, raw.modes.light )?;
let dark = mode_from_raw( root, raw.modes.dark )?;
Ok( ThemeDocument
{
id: raw.theme.id,
name: raw.theme.name,
root: root.map( Path::to_path_buf ),
fonts,
light,
dark,
})
}
pub fn load_document_from_dir( dir: &Path ) -> Result<ThemeDocument, ThemeError>
{
let json_path = dir.join( "theme.json" );
let text = std::fs::read_to_string( &json_path )
.map_err( |e| ThemeError::Io( json_path.clone(), e ) )?;
parse_document_json( &text, Some( dir ) )
}
fn resolve_relative( root: Option<&Path>, rel: &str ) -> PathBuf
{
let p = Path::new( rel );
if p.is_absolute() { return p.to_path_buf(); }
match root
{
Some( r ) => r.join( p ),
None => p.to_path_buf(),
}
}