diff --git a/common/src/main/java/com/android/designcompose/common/FsaasSession.kt b/common/src/main/java/com/android/designcompose/common/FsaasSession.kt index 2b1f68e32..7aa779f52 100644 --- a/common/src/main/java/com/android/designcompose/common/FsaasSession.kt +++ b/common/src/main/java/com/android/designcompose/common/FsaasSession.kt @@ -17,4 +17,4 @@ package com.android.designcompose.common // Current serialized doc version -const val FSAAS_DOC_VERSION = 18 +const val FSAAS_DOC_VERSION = 19 diff --git a/crates/figma_import/src/bin/fetch.rs b/crates/figma_import/src/bin/fetch.rs index c4ae95012..0bbcfb56b 100644 --- a/crates/figma_import/src/bin/fetch.rs +++ b/crates/figma_import/src/bin/fetch.rs @@ -88,6 +88,9 @@ fn fetch_impl(args: Args) -> Result<(), ConvertError> { for error in error_list { eprintln!("Warning: {error}"); } + + let variable_map = doc.build_variable_map(); + // Build the serializable doc structure let serializable_doc = SerializedDesignDoc { views, @@ -97,6 +100,7 @@ fn fetch_impl(args: Args) -> Result<(), ConvertError> { name: doc.get_name(), version: doc.get_version(), id: doc.get_document_id(), + variable_map: variable_map, }; println!("Fetched document"); println!(" DC Version: {}", SerializedDesignDocHeader::current().version); diff --git a/crates/figma_import/src/bin/fetch_layout.rs b/crates/figma_import/src/bin/fetch_layout.rs index 68326d04a..bceb42e5d 100644 --- a/crates/figma_import/src/bin/fetch_layout.rs +++ b/crates/figma_import/src/bin/fetch_layout.rs @@ -254,6 +254,8 @@ fn fetch_impl(args: Args) -> Result<(), ConvertError> { } */ + let variable_map = doc.build_variable_map(); + // Build the serializable doc structure let serializable_doc = SerializedDesignDoc { views, @@ -263,6 +265,7 @@ fn fetch_impl(args: Args) -> Result<(), ConvertError> { name: doc.get_name(), version: doc.get_version(), id: doc.get_document_id(), + variable_map: variable_map, }; // We don't bother with serialization headers or image sessions with diff --git a/crates/figma_import/src/document.rs b/crates/figma_import/src/document.rs index 812ef7674..8ac371b69 100644 --- a/crates/figma_import/src/document.rs +++ b/crates/figma_import/src/document.rs @@ -26,10 +26,13 @@ use crate::{ fetch::ProxyConfig, figma_schema::{ Component, ComponentKeyResponse, FileHeadResponse, FileResponse, ImageFillResponse, Node, - NodeData, NodesResponse, ProjectFilesResponse, + NodeData, NodesResponse, ProjectFilesResponse, VariablesResponse, }, image_context::{EncodedImageMap, ImageContext, ImageContextSession, ImageKey}, - toolkit_schema::{ComponentContentOverride, ComponentOverrides, View, ViewData}, + toolkit_schema::{ + Collection, ComponentContentOverride, ComponentOverrides, Mode, Variable, VariableMap, + View, ViewData, + }, transform_flexbox::create_component_flexbox, }; @@ -142,6 +145,7 @@ pub struct Document { version_id: String, proxy_config: ProxyConfig, document_root: FileResponse, + variables_response: VariablesResponse, image_context: ImageContext, variant_nodes: Vec, component_sets: HashMap, @@ -183,6 +187,7 @@ impl Document { } let branches = get_branches(&document_root); + let variables_response = Self::fetch_variables(api_key, &document_id, proxy_config)?; Ok(Document { api_key: api_key.to_string(), @@ -190,6 +195,7 @@ impl Document { version_id, proxy_config: proxy_config.clone(), document_root, + variables_response, image_context, variant_nodes: vec![], component_sets: HashMap::new(), @@ -197,6 +203,18 @@ impl Document { }) } + // Fetch and store all the variables, collections, and modes from the Figma document. + fn fetch_variables( + api_key: &str, + document_id: &String, + proxy_config: &ProxyConfig, + ) -> Result { + let variables_url = format!("{}{}/variables/local", BASE_FILE_URL, document_id); + let var_fetch = http_fetch(api_key, variables_url, proxy_config)?; + let var_response: VariablesResponse = serde_json::from_str(var_fetch.as_str())?; + Ok(var_response) + } + /// Fetch a document from Figma only if it has changed since the given last /// modified time. pub fn new_if_changed( @@ -873,6 +891,55 @@ impl Document { Ok(views) } + // Parse through all the variables collected and store them into hash tables for easy access + pub fn build_variable_map(&self) -> VariableMap { + let mut collections: HashMap = HashMap::new(); + let mut collection_name_map: HashMap = HashMap::new(); + for (_, c) in self.variables_response.meta.variable_collections.iter() { + let mut mode_name_hash: HashMap = HashMap::new(); + let mut mode_id_hash: HashMap = HashMap::new(); + for m in &c.modes { + let mode = Mode { id: m.mode_id.clone(), name: m.name.clone() }; + mode_name_hash.insert(mode.name.clone(), mode.id.clone()); + mode_id_hash.insert(mode.id.clone(), mode); + } + let collection = Collection { + id: c.id.clone(), + name: c.name.clone(), + default_mode_id: c.default_mode_id.clone(), + mode_name_hash: mode_name_hash, + mode_id_hash: mode_id_hash, + }; + collections.insert(collection.id.clone(), collection); + collection_name_map.insert(c.name.clone(), c.id.clone()); + } + + let mut variables: HashMap = HashMap::new(); + let mut variable_name_map: HashMap> = HashMap::new(); + for (id, v) in self.variables_response.meta.variables.iter() { + let var = Variable::from_figma_var(v); + let maybe_name_map = variable_name_map.get_mut(&var.variable_collection_id); + if let Some(name_map) = maybe_name_map { + name_map.insert(var.name.clone(), id.clone()); + } else { + let mut name_to_id = HashMap::new(); + name_to_id.insert(var.name.clone(), id.clone()); + variable_name_map.insert(var.variable_collection_id.clone(), name_to_id); + } + + variables.insert(id.clone(), var); + } + + let var_map = VariableMap { + collections, + collection_name_map, + variables: variables.clone(), + variable_name_map, + }; + + var_map + } + /// Return the EncodedImageMap containing the mapping from ImageKey references in Nodes returned by this document /// to encoded image bytes. EncodedImageMap can be serialized and deserialized, and transformed into an ImageMap. pub fn encoded_image_map(&self) -> EncodedImageMap { diff --git a/crates/figma_import/src/fetch.rs b/crates/figma_import/src/fetch.rs index d2f423108..c1a4d8547 100644 --- a/crates/figma_import/src/fetch.rs +++ b/crates/figma_import/src/fetch.rs @@ -99,6 +99,8 @@ pub fn fetch_doc( &mut error_list, )?; + let variable_map = doc.build_variable_map(); + let figma_doc = SerializedDesignDoc { views, component_sets: doc.component_sets().clone(), @@ -107,6 +109,7 @@ pub fn fetch_doc( name: doc.get_name(), version: doc.get_version(), id: doc.get_document_id(), + variable_map: variable_map, }; let mut response = bincode::serialize(&SerializedDesignDocHeader::current())?; response.append(&mut bincode::serialize(&ServerFigmaDoc { diff --git a/crates/figma_import/src/figma_schema.rs b/crates/figma_import/src/figma_schema.rs index 8c5982487..e97058d47 100644 --- a/crates/figma_import/src/figma_schema.rs +++ b/crates/figma_import/src/figma_schema.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use crate::color::Color; use crate::vector_schema::WindingRule; // We use serde to decode Figma's JSON documents into Rust structures. @@ -33,6 +34,11 @@ pub struct FigmaColor { pub b: f32, pub a: f32, } +impl Into for &FigmaColor { + fn into(self) -> Color { + Color::from_f32s(self.r, self.g, self.b, self.a) + } +} #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] @@ -472,6 +478,8 @@ pub struct Gradient { pub enum PaintData { Solid { color: FigmaColor, + #[serde(rename = "boundVariables")] + bound_variables: Option, }, GradientLinear { #[serde(flatten)] @@ -904,6 +912,8 @@ pub struct Node { pub stroke_geometry: Option>, #[serde(default = "default_stroke_cap")] pub stroke_cap: StrokeCap, + pub bound_variables: Option, + pub explicit_variable_modes: Option>, } impl Node { @@ -1070,3 +1080,164 @@ pub struct ProjectFilesResponse { pub name: String, pub files: Vec>, } + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ModeBinding { + pub mode_id: String, + pub name: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VariableCollection { + pub default_mode_id: String, + pub id: String, + pub name: String, + pub remote: bool, + pub modes: Vec, + pub key: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VariableType { + Boolean, + Float, + String, + Color, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct VariableAlias { + pub r#type: String, + pub id: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum VariableAliasOrList { + Alias(VariableAlias), + List(Vec), +} +impl VariableAliasOrList { + fn get_name(&self) -> Option { + match self { + VariableAliasOrList::Alias(alias) => { + return Some(alias.id.clone()); + } + VariableAliasOrList::List(list) => { + let alias = list.first(); + if let Some(alias) = alias { + return Some(alias.id.clone()); + } + } + } + None + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BoundVariables { + #[serde(flatten)] + variables: HashMap, +} +impl BoundVariables { + pub(crate) fn has_var(&self, var_name: &str) -> bool { + self.variables.contains_key(var_name) + } + + pub(crate) fn get_variable(&self, var_name: &str) -> Option { + let var = self.variables.get(var_name); + if let Some(var) = var { + return var.get_name(); + } + None + } +} + +// We use the "untagged" serde attribute because the value of a variable is +// described in a hash where we don't know the format of the value based on the +// key. The value format is different depending on the type of variable and if +// it is an alias to another variable. For example, this is a string value +// +// "valuesByMode": { +// "1:0": "Hello" +// } +// +// And this is an alias to another variable: +// +// "valuesByMode": { +// "1:0": { +// "type": "VARIABLE_ALIAS", +// "id": "VariableID:1:234" +// }, +// } +// +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(untagged)] +pub enum VariableValue { + Boolean(bool), + Float(f32), + String(String), + Color(FigmaColor), + Alias(VariableAlias), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VariableCommon { + pub id: String, + pub name: String, + pub remote: bool, + pub key: String, + pub variable_collection_id: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] // Because "resolvedType" is in this format +#[serde(tag = "resolvedType")] // Maps to different enum values based on "resolvedType" +pub enum Variable { + #[serde(rename_all = "camelCase")] // Because the members in each enum are in this format + Boolean { + #[serde(flatten)] + common: VariableCommon, + values_by_mode: HashMap, + }, + #[serde(rename_all = "camelCase")] + Float { + #[serde(flatten)] + common: VariableCommon, + values_by_mode: HashMap, + }, + #[serde(rename_all = "camelCase")] + String { + #[serde(flatten)] + common: VariableCommon, + values_by_mode: HashMap, + }, + #[serde(rename_all = "camelCase")] + Color { + #[serde(flatten)] + common: VariableCommon, + //#[serde(deserialize_with = "value_or_alias")] + values_by_mode: HashMap, + }, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VariablesMeta { + pub variable_collections: HashMap, + pub variables: HashMap, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VariablesResponse { + pub error: bool, + pub status: i32, + pub meta: VariablesMeta, +} diff --git a/crates/figma_import/src/reflection.rs b/crates/figma_import/src/reflection.rs index 726333b70..412d7afc5 100644 --- a/crates/figma_import/src/reflection.rs +++ b/crates/figma_import/src/reflection.rs @@ -153,6 +153,34 @@ pub fn registry() -> serde_reflection::Result { tracer .trace_type::(&samples) .expect("couldn't trace StrokeCap"); + tracer.trace_type::(&samples).expect("couldn't trace Mode"); + tracer + .trace_type::(&samples) + .expect("couldn't trace Collection"); + tracer + .trace_type::(&samples) + .expect("couldn't trace VariableType"); + tracer + .trace_type::(&samples) + .expect("couldn't trace FigmaColor"); + tracer + .trace_type::(&samples) + .expect("couldn't trace VariableAlias"); + tracer + .trace_type::(&samples) + .expect("couldn't trace NumOrVar"); + tracer + .trace_type::(&samples) + .expect("couldn't trace ColorOrVar"); + tracer + .trace_type::(&samples) + .expect("couldn't trace VariableValue"); + tracer + .trace_type::(&samples) + .expect("couldn't trace Variable"); + tracer + .trace_type::(&samples) + .expect("couldn't trace VariableMap"); tracer .trace_type::(&samples) .expect("couldn't trace ViewShape"); diff --git a/crates/figma_import/src/serialized_document.rs b/crates/figma_import/src/serialized_document.rs index d9cbf94c8..f75e94c01 100644 --- a/crates/figma_import/src/serialized_document.rs +++ b/crates/figma_import/src/serialized_document.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; use crate::{document::FigmaDocInfo, image_context::EncodedImageMap, toolkit_schema, NodeQuery}; -static CURRENT_VERSION: u32 = 18; +static CURRENT_VERSION: u32 = 19; // This is our serialized document type. #[derive(Serialize, Deserialize, Debug)] @@ -54,6 +54,7 @@ pub struct SerializedDesignDoc { pub component_sets: HashMap, pub version: String, pub id: String, + pub variable_map: toolkit_schema::VariableMap, } impl fmt::Display for SerializedDesignDoc { diff --git a/crates/figma_import/src/toolkit_schema.rs b/crates/figma_import/src/toolkit_schema.rs index c81df5b1b..1f7f952ed 100644 --- a/crates/figma_import/src/toolkit_schema.rs +++ b/crates/figma_import/src/toolkit_schema.rs @@ -19,13 +19,56 @@ use std::sync::atomic::AtomicU16; // retain image references. use serde::{Deserialize, Serialize}; -use crate::figma_schema::Rectangle; +use crate::color::Color; +use crate::figma_schema; +use crate::figma_schema::VariableCommon; use crate::reaction_schema::FrameExtras; use crate::reaction_schema::Reaction; use crate::toolkit_style::{StyledTextRun, ViewStyle}; +use std::collections::HashMap; -pub use crate::figma_schema::OverflowDirection; -pub use crate::figma_schema::StrokeCap; +pub use crate::figma_schema::{FigmaColor, OverflowDirection, Rectangle, StrokeCap, VariableAlias}; + +// Enum for fields that represent either a fixed number or a number variable +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum NumOrVar { + Num(f32), + Var(String), +} +impl NumOrVar { + pub(crate) fn from_var( + bound_variables: &figma_schema::BoundVariables, + var_name: &str, + num: f32, + ) -> NumOrVar { + let var = bound_variables.get_variable(var_name); + if let Some(var) = var { + NumOrVar::Var(var) + } else { + NumOrVar::Num(num) + } + } +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum ColorOrVar { + Color(Color), + Var(String), +} +impl ColorOrVar { + pub(crate) fn from_var( + bound_variables: &figma_schema::BoundVariables, + var_name: &str, + color: &FigmaColor, + ) -> ColorOrVar { + let var = bound_variables.get_variable(var_name); + if let Some(var) = var { + ColorOrVar::Var(var) + } else { + ColorOrVar::Color(color.into()) + } + } +} /// Shape of a view, either a rect or a path of some kind. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] @@ -34,7 +77,7 @@ pub enum ViewShape { is_mask: bool, }, RoundRect { - corner_radius: [f32; 4], + corner_radius: [NumOrVar; 4], corner_smoothing: f32, is_mask: bool, }, @@ -56,7 +99,7 @@ pub enum ViewShape { VectorRect { path: Vec, stroke: Vec, - corner_radius: [f32; 4], + corner_radius: [NumOrVar; 4], is_mask: bool, }, } @@ -144,6 +187,8 @@ pub struct View { pub data: ViewData, pub design_absolute_bounding_box: Option, pub render_method: RenderMethod, + // a hash of variable collection id -> mode id if variable modes are set on this view + pub explicit_variable_modes: Option>, } impl View { fn next_unique_id() -> u16 { @@ -161,6 +206,7 @@ impl View { frame_extras: Option, design_absolute_bounding_box: Option, render_method: RenderMethod, + explicit_variable_modes: Option>, ) -> View { View { unique_id: View::next_unique_id(), @@ -174,6 +220,7 @@ impl View { data: ViewData::Container { shape, children: vec![] }, design_absolute_bounding_box, render_method, + explicit_variable_modes, } } pub(crate) fn new_text( @@ -198,6 +245,7 @@ impl View { data: ViewData::Text { content: text.into() }, design_absolute_bounding_box, render_method, + explicit_variable_modes: None, } } pub(crate) fn new_styled_text( @@ -222,6 +270,7 @@ impl View { data: ViewData::StyledText { content: text }, design_absolute_bounding_box, render_method, + explicit_variable_modes: None, } } pub(crate) fn add_child(&mut self, child: View) { @@ -230,3 +279,130 @@ impl View { } } } + +// Representation of a variable mode. Variables can have fixed values for each available mode +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Mode { + pub id: String, + pub name: String, +} + +// Representation of a variable collection. Every variable belongs to a collection, and a +// collection contains one or more modes. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Collection { + pub id: String, + pub name: String, + pub default_mode_id: String, + pub mode_name_hash: HashMap, // name -> id + pub mode_id_hash: HashMap, // id -> Mode +} + +// We redeclare VariableValue instead of using the one from figma_schema because +// the "untagged" attribute there prevents serde_reflection from being able to +// run properly. +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum VariableValue { + Bool(bool), + Number(f32), + Text(String), + Color(Color), + Alias(VariableAlias), +} +impl VariableValue { + fn from_figma_value(v: &figma_schema::VariableValue) -> VariableValue { + match v { + figma_schema::VariableValue::Boolean(b) => VariableValue::Bool(b.clone()), + figma_schema::VariableValue::Float(f) => VariableValue::Number(f.clone()), + figma_schema::VariableValue::String(s) => VariableValue::Text(s.clone()), + figma_schema::VariableValue::Color(c) => VariableValue::Color(c.into()), + figma_schema::VariableValue::Alias(a) => VariableValue::Alias(a.clone()), + } + } +} + +// Each variable contains a map of possible values. This data structure helps +// keep track of that data and contains functions to retrieve the value of a +// variable given a mode. +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct VariableValueMap { + pub values_by_mode: HashMap, +} +impl VariableValueMap { + fn from_figma_map(map: &HashMap) -> VariableValueMap { + let mut values_by_mode: HashMap = HashMap::new(); + for (mode_id, value) in map.iter() { + values_by_mode.insert(mode_id.clone(), VariableValue::from_figma_value(value)); + } + VariableValueMap { values_by_mode } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum VariableType { + Bool, + Number, + Text, + Color, +} + +// Representation of a Figma variable. We convert a figma_schema::Variable into +// this format to make the fields a bit easier to access. +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Variable { + pub id: String, + pub name: String, + pub remote: bool, + pub key: String, + pub variable_collection_id: String, + pub var_type: VariableType, + pub values_by_mode: VariableValueMap, +} +impl Variable { + fn new( + var_type: VariableType, + common: &VariableCommon, + values_by_mode: &HashMap, + ) -> Self { + Variable { + id: common.id.clone(), + name: common.name.clone(), + remote: common.remote, + key: common.key.clone(), + variable_collection_id: common.variable_collection_id.clone(), + var_type, + values_by_mode: VariableValueMap::from_figma_map(values_by_mode), + } + } + pub fn from_figma_var(v: &figma_schema::Variable) -> Variable { + match v { + figma_schema::Variable::Boolean { common, values_by_mode } => { + Variable::new(VariableType::Bool, common, values_by_mode) + } + figma_schema::Variable::Float { common, values_by_mode } => { + Variable::new(VariableType::Number, common, values_by_mode) + } + figma_schema::Variable::String { common, values_by_mode } => { + Variable::new(VariableType::Bool, common, values_by_mode) + } + figma_schema::Variable::Color { common, values_by_mode } => { + Variable::new(VariableType::Text, common, values_by_mode) + } + } + } +} + +/// Stores variable mappings +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VariableMap { + pub collections: HashMap, // ID -> Collection + pub collection_name_map: HashMap, // Name -> ID + pub variables: HashMap, // ID -> Variable + pub variable_name_map: HashMap>, // Collection ID -> [Name -> ID] +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BoundVariables { + pub variables: HashMap, +} diff --git a/crates/figma_import/src/toolkit_style.rs b/crates/figma_import/src/toolkit_style.rs index 9a9b12cde..be8546d7f 100644 --- a/crates/figma_import/src/toolkit_style.rs +++ b/crates/figma_import/src/toolkit_style.rs @@ -24,6 +24,7 @@ use crate::{ color::Color, toolkit_font_style::{FontStretch, FontStyle, FontWeight}, toolkit_layout_style::{Display, FlexWrap, LayoutSizing, Number, Overflow}, + toolkit_schema::ColorOrVar, }; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] @@ -39,7 +40,7 @@ pub enum ScaleMode { pub enum Background { #[default] None, - Solid(Color), + Solid(ColorOrVar), LinearGradient { start_x: f32, start_y: f32, @@ -122,7 +123,7 @@ pub struct TextStyle { impl Default for TextStyle { fn default() -> Self { TextStyle { - text_color: Background::Solid(Color::BLACK), + text_color: Background::Solid(ColorOrVar::Color(Color::BLACK)), font_size: 18.0, font_family: None, font_weight: FontWeight::NORMAL, @@ -528,7 +529,7 @@ pub struct NodeStyle { impl Default for NodeStyle { fn default() -> NodeStyle { NodeStyle { - text_color: Background::Solid(Color::from_u8s(0, 0, 0, 255)), + text_color: Background::Solid(ColorOrVar::Color(Color::from_u8s(0, 0, 0, 255))), font_size: 18.0, font_family: None, font_weight: FontWeight::NORMAL, diff --git a/crates/figma_import/src/transform_flexbox.rs b/crates/figma_import/src/transform_flexbox.rs index 9e96bd47b..a90c2ce39 100644 --- a/crates/figma_import/src/transform_flexbox.rs +++ b/crates/figma_import/src/transform_flexbox.rs @@ -18,7 +18,6 @@ use std::f32::consts::PI; use crate::toolkit_font_style::{FontStyle, FontWeight}; use crate::toolkit_layout_style::{FlexWrap, Overflow}; -use crate::toolkit_schema::ViewShape; use crate::toolkit_style::{ Background, FilterOp, FontFeature, GridLayoutType, GridSpan, LayoutTransform, LineHeight, MeterData, ShadowBox, StyledTextRun, TextAlign, TextOverflow, TextStyle, ViewStyle, @@ -36,7 +35,10 @@ use crate::{ }, image_context::ImageContext, reaction_schema::{FrameExtras, Reaction, ReactionJson}, - toolkit_schema::{ComponentInfo, OverflowDirection, RenderMethod, ScrollInfo, View}, + toolkit_schema::{ + ColorOrVar, ComponentInfo, NumOrVar, OverflowDirection, RenderMethod, ScrollInfo, View, + ViewShape, + }, }; use layout::styles::{ @@ -511,13 +513,18 @@ fn compute_background( images: &mut ImageContext, node_name: &String, ) -> crate::toolkit_style::Background { - if let PaintData::Solid { color } = &last_paint.data { - Background::Solid(crate::Color::from_f32s( - color.r, - color.g, - color.b, - color.a * last_paint.opacity, - )) + if let PaintData::Solid { color, bound_variables } = &last_paint.data { + let solid_bg = if let Some(vars) = bound_variables { + ColorOrVar::from_var(vars, "color", color) + } else { + ColorOrVar::Color(crate::Color::from_f32s( + color.r, + color.g, + color.b, + color.a * last_paint.opacity, + )) + }; + Background::Solid(solid_bg) } else if let PaintData::Image { image_ref: Some(image_ref), filters, @@ -1277,25 +1284,53 @@ fn visit_node( Vec::new() }; - let (corner_radius, has_corner_radius) = if let Some(border_radii) = node.rectangle_corner_radii - { - ( - [border_radii[0], border_radii[1], border_radii[2], border_radii[3]], - border_radii[0] > 0.0 - || border_radii[1] > 0.0 - || border_radii[2] > 0.0 - || border_radii[3] > 0.0, - ) - } else if let Some(border_radius) = node.corner_radius { - ([border_radius, border_radius, border_radius, border_radius], border_radius > 0.0) + // Get the raw number values for the corner radii. If any are non-zero, set the boolean + // has_corner_radius to true. + let (corner_radius_values, mut has_corner_radius) = + if let Some(border_radii) = node.rectangle_corner_radii { + // rectangle_corner_radii is set if the corner radii are not all the same + ( + [border_radii[0], border_radii[1], border_radii[2], border_radii[3]], + border_radii[0] > 0.0 + || border_radii[1] > 0.0 + || border_radii[2] > 0.0 + || border_radii[3] > 0.0, + ) + } else if let Some(border_radius) = node.corner_radius { + // corner_radius is set if all four corners are set to the same value + ([border_radius, border_radius, border_radius, border_radius], border_radius > 0.0) + } else { + ([0.0, 0.0, 0.0, 0.0], false) + }; + + // Collect the corner radii values to be saved into the view. If the corner radii are set + // to variables, they will be set to NumOrVar::Var. Otherwise they will be set to NumOrVar::Num. + let corner_radius = if let Some(vars) = &node.bound_variables { + let top_left = NumOrVar::from_var(vars, "topLeftRadius", corner_radius_values[0]); + let top_right = NumOrVar::from_var(vars, "topRightRadius", corner_radius_values[1]); + let bottom_right = NumOrVar::from_var(vars, "bottomRightRadius", corner_radius_values[2]); + let bottom_left = NumOrVar::from_var(vars, "bottomLeftRadius", corner_radius_values[3]); + if vars.has_var("topLeftRadius") + || vars.has_var("topRightRadius") + || vars.has_var("bottomRightRadius") + || vars.has_var("bottomLeftRadius") + { + has_corner_radius = true; + } + [top_left, top_right, bottom_right, bottom_left] } else { - ([0.0, 0.0, 0.0, 0.0], false) + [ + NumOrVar::Num(corner_radius_values[0]), + NumOrVar::Num(corner_radius_values[1]), + NumOrVar::Num(corner_radius_values[2]), + NumOrVar::Num(corner_radius_values[3]), + ] }; let make_rect = |is_mask| -> ViewShape { if has_corner_radius { ViewShape::RoundRect { - corner_radius, + corner_radius: corner_radius.clone(), corner_smoothing: 0.0, // Not in Figma REST API is_mask: is_mask, } @@ -1320,7 +1355,7 @@ fn visit_node( NodeData::Rectangle { vector } => ViewShape::VectorRect { path: fill_paths, stroke: stroke_paths, - corner_radius, + corner_radius: corner_radius, is_mask: vector.is_mask, }, // Ellipses get turned into an Arc in order to support dials/gauges with an arc type @@ -1334,7 +1369,8 @@ fn visit_node( start_angle_degrees: euclid::Angle::radians(arc_data.starting_angle).to_degrees(), sweep_angle_degrees: euclid::Angle::radians(arc_data.ending_angle).to_degrees(), inner_radius: arc_data.inner_radius, - corner_radius: 0.0, // corner radius is only exposed in the plugin data + // corner radius for arcs in dials & gauges does not support variables yet + corner_radius: 0.0, is_mask: vector.is_mask, }, NodeData::Frame { frame } @@ -1408,6 +1444,7 @@ fn visit_node( frame_extras, node.absolute_bounding_box, RenderMethod::None, + node.explicit_variable_modes.clone(), ); // Iterate over our visible children, but not vectors because they always diff --git a/crates/figma_import/tests/layout-unit-tests.dcf b/crates/figma_import/tests/layout-unit-tests.dcf index 883f0f46e..beab1c235 100644 Binary files a/crates/figma_import/tests/layout-unit-tests.dcf and b/crates/figma_import/tests/layout-unit-tests.dcf differ diff --git a/designcompose/build.gradle.kts b/designcompose/build.gradle.kts index 3cf31a9d9..483b6330a 100644 --- a/designcompose/build.gradle.kts +++ b/designcompose/build.gradle.kts @@ -110,6 +110,7 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.compose.runtime.livedata) + implementation(libs.androidx.compose.material3) implementation(libs.androidx.datastore.core) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.lifecycle.runtime) diff --git a/designcompose/src/main/assets/figma/DesignSwitcherDoc_Ljph4e3sC0lHcynfXpoh9f.dcf b/designcompose/src/main/assets/figma/DesignSwitcherDoc_Ljph4e3sC0lHcynfXpoh9f.dcf index 12112d415..dcb5d2f5c 100644 Binary files a/designcompose/src/main/assets/figma/DesignSwitcherDoc_Ljph4e3sC0lHcynfXpoh9f.dcf and b/designcompose/src/main/assets/figma/DesignSwitcherDoc_Ljph4e3sC0lHcynfXpoh9f.dcf differ diff --git a/designcompose/src/main/java/com/android/designcompose/ComputePaths.kt b/designcompose/src/main/java/com/android/designcompose/ComputePaths.kt index 5165f81b9..a2f176b37 100644 --- a/designcompose/src/main/java/com/android/designcompose/ComputePaths.kt +++ b/designcompose/src/main/java/com/android/designcompose/ComputePaths.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.asComposePath import androidx.core.graphics.minus import androidx.core.graphics.plus import com.android.designcompose.serdegen.BoxShadow +import com.android.designcompose.serdegen.NumOrVar import com.android.designcompose.serdegen.StrokeAlign import com.android.designcompose.serdegen.StrokeCap import com.android.designcompose.serdegen.ViewShape @@ -76,6 +77,7 @@ internal fun ViewShape.computePaths( vectorScaleX: Float, vectorScaleY: Float, layoutId: Int, + variableState: VariableState, ): ComputedPaths { fun getPaths( path: List, @@ -98,9 +100,15 @@ internal fun ViewShape.computePaths( is ViewShape.Rect -> { return computeRoundRectPathsFast( style, - listOf(0.0f, 0.0f, 0.0f, 0.0f), + listOf( + NumOrVar.Num(0.0f), + NumOrVar.Num(0.0f), + NumOrVar.Num(0.0f), + NumOrVar.Num(0.0f) + ), density, getRectSize(overrideSize, style, density), + variableState ) } is ViewShape.RoundRect -> { @@ -108,7 +116,8 @@ internal fun ViewShape.computePaths( style, this.corner_radius, density, - getRectSize(overrideSize, style, density) + getRectSize(overrideSize, style, density), + variableState ) } is ViewShape.VectorRect -> { @@ -116,7 +125,8 @@ internal fun ViewShape.computePaths( style, this.corner_radius, density, - getRectSize(overrideSize, style, density) + getRectSize(overrideSize, style, density), + variableState ) } is ViewShape.Path -> { @@ -538,20 +548,25 @@ private fun Path.addRoundRect(roundRect: RoundRect, dir: android.graphics.Path.D /// faster. private fun computeRoundRectPathsFast( style: ViewStyle, - cornerRadius: List, + cornerRadius: List, density: Float, - frameSize: Size + frameSize: Size, + variableState: VariableState, ): ComputedPaths { + val cornerRadius0 = cornerRadius[0].getValue(variableState) + val cornerRadius1 = cornerRadius[1].getValue(variableState) + val cornerRadius2 = cornerRadius[2].getValue(variableState) + val cornerRadius3 = cornerRadius[3].getValue(variableState) val r = RoundRect( 0.0f, 0.0f, frameSize.width, frameSize.height, - CornerRadius(cornerRadius[0] * density, cornerRadius[0] * density), - CornerRadius(cornerRadius[1] * density, cornerRadius[1] * density), - CornerRadius(cornerRadius[2] * density, cornerRadius[2] * density), - CornerRadius(cornerRadius[3] * density, cornerRadius[3] * density) + CornerRadius(cornerRadius0 * density, cornerRadius0 * density), + CornerRadius(cornerRadius1 * density, cornerRadius1 * density), + CornerRadius(cornerRadius2 * density, cornerRadius2 * density), + CornerRadius(cornerRadius3 * density, cornerRadius3 * density) ) val strokeInsets = diff --git a/designcompose/src/main/java/com/android/designcompose/CustomizationContext.kt b/designcompose/src/main/java/com/android/designcompose/CustomizationContext.kt index a93880cdc..b3f93433b 100644 --- a/designcompose/src/main/java/com/android/designcompose/CustomizationContext.kt +++ b/designcompose/src/main/java/com/android/designcompose/CustomizationContext.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.text.TextStyle import com.android.designcompose.common.nodeNameToPropertyValueList import com.android.designcompose.serdegen.Background +import com.android.designcompose.serdegen.ColorOrVar import com.android.designcompose.serdegen.ComponentInfo import com.android.designcompose.serdegen.Dimension import com.android.designcompose.serdegen.NodeQuery @@ -104,7 +105,7 @@ data class Customization( // Meter (dial, gauge, progress bar) customization as a percentage 0-100 var meterValue: Optional = Optional.empty(), // Meter (dial, gauge, progress bar) customization as a function that returns a percentage 0-100 - var meterFunction: Optional<@Composable () -> Float> = Optional.empty() + var meterFunction: Optional<@Composable () -> Float> = Optional.empty(), ) private fun Customization.clone(): Customization { @@ -175,11 +176,14 @@ data class ImageContext( ) { fun getBackgroundColor(): Int? { if (background.size == 1 && background[0] is Background.Solid) { - val color = (background[0] as Background.Solid).value.color - return ((color[3].toInt() shl 24) and 0xFF000000.toInt()) or - ((color[0].toInt() shl 16) and 0x00FF0000) or - ((color[1].toInt() shl 8) and 0x0000FF00) or - (color[2].toInt() and 0x000000FF) + val color = (background[0] as Background.Solid).value + if (color is ColorOrVar.Color) { + val color = color.value.color + return ((color[3].toInt() shl 24) and 0xFF000000.toInt()) or + ((color[0].toInt() shl 16) and 0x00FF0000) or + ((color[1].toInt() shl 8) and 0x0000FF00) or + (color[2].toInt() and 0x000000FF) + } } return null } diff --git a/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt b/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt index 8256a0c69..c2e2a3ca5 100644 --- a/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt +++ b/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt @@ -194,6 +194,17 @@ internal fun DesignFrame( ) } + // if the developer has not explicitly set variable override values, check to see if any + // variable modes have been set on this node. If so, collect the values in modeValues to be used + // to resolve variable values for this node and children + var modeValues: VariableModeValues? = null + if (!LocalVariableState.hasOverrideModeValues()) { + if (view.explicit_variable_modes.isPresent) { + val modes = view.explicit_variable_modes.get() + modeValues = VariableManager.updateVariableStateFromModeValues(modes) + } + } + // Since the meter function is a composable, we need to call it here even though we don't need // it until frameRender() since that function is not a composable. val meterValue = customizations.getMeterFunction(name)?.let { it() } @@ -203,7 +214,8 @@ internal fun DesignFrame( // Only render the frame if we don't have a replacement node and layout is absolute val shape = (view.data as ViewData.Container).shape - if (replacementComponent == null && layoutInfo.shouldRender()) + if (replacementComponent == null && layoutInfo.shouldRender()) { + val varMaterialState = VariableState.create(modeValues) m = m.frameRender( style, @@ -213,8 +225,10 @@ internal fun DesignFrame( name, customizations, maskInfo, - layoutId + layoutId, + varMaterialState, ) + } val lazyContent = customizations.getListContent(name) @@ -682,8 +696,10 @@ internal fun DesignFrame( // Use our custom layout to render the frame and to place its children m = m.then(Modifier.layoutStyle(name, layoutId)) m = m.then(layoutInfo.selfModifier) - DesignFrameLayout(m, view, layoutId, rootLayoutId, layoutState, designScroll) { - content() + DesignVariableModeValues(modeValues) { + DesignFrameLayout(m, view, layoutId, rootLayoutId, layoutState, designScroll) { + content() + } } } } @@ -701,6 +717,7 @@ internal fun Modifier.frameRender( customizations: CustomizationContext, maskInfo: MaskInfo?, layoutId: Int, + variableState: VariableState, ): Modifier = this.then( Modifier.drawWithContent { @@ -713,7 +730,8 @@ internal fun Modifier.frameRender( document, name, customizations, - layoutId + layoutId, + variableState, ) when (maskInfo?.type ?: MaskViewType.None) { diff --git a/designcompose/src/main/java/com/android/designcompose/DesignText.kt b/designcompose/src/main/java/com/android/designcompose/DesignText.kt index 30b37bb5f..204c5d966 100644 --- a/designcompose/src/main/java/com/android/designcompose/DesignText.kt +++ b/designcompose/src/main/java/com/android/designcompose/DesignText.kt @@ -133,12 +133,14 @@ internal fun DesignText( val fontFamily = DesignSettings.fontFamily(style.node_style.font_family) val customTextStyle = customizations.getTextStyle(nodeName) val textBuilder = AnnotatedString.Builder() + val variableState = VariableState.create() if (useText != null) { textBuilder.append(useText) } else if (runs != null) { for (run in runs) { - val textBrushAndOpacity = run.style.text_color.asBrush(document, density.density) + val textBrushAndOpacity = + run.style.text_color.asBrush(document, density.density, variableState) textBuilder.pushStyle( @OptIn(ExperimentalTextApi::class) SpanStyle( @@ -217,7 +219,8 @@ internal fun DesignText( customizations.getBrush(nodeName) } - val textBrushAndOpacity = style.node_style.text_color.asBrush(document, density.density) + val textBrushAndOpacity = + style.node_style.text_color.asBrush(document, density.density, variableState) val textStyle = @OptIn(ExperimentalTextApi::class) TextStyle( diff --git a/designcompose/src/main/java/com/android/designcompose/DocServer.kt b/designcompose/src/main/java/com/android/designcompose/DocServer.kt index bd9bd581e..7f5528d26 100644 --- a/designcompose/src/main/java/com/android/designcompose/DocServer.kt +++ b/designcompose/src/main/java/com/android/designcompose/DocServer.kt @@ -333,6 +333,8 @@ internal fun DocServer.fetchDocuments( DesignSettings.fileFetchStatus[id]?.lastUpdateFromFetch = now } + VariableManager.init(doc.c.docId.id, doc.c.document.variable_map) + // Get the list of subscribers to this document id val subs: Array = synchronized(subscriptions) { @@ -344,10 +346,7 @@ internal fun DocServer.fetchDocuments( SpanCache.clear() for (subscriber in subs) { subscriber.onUpdate(doc) - subscriber.docUpdateCallback?.invoke( - id, - doc?.c?.toSerializedBytes(Feedback) - ) + subscriber.docUpdateCallback?.invoke(id, doc.c?.toSerializedBytes(Feedback)) } } Feedback.documentUpdated(id, subs.size) @@ -488,6 +487,7 @@ internal fun DocServer.doc( } targetDoc } + targetDoc?.let { VariableManager.init(it.c.docId.id, it.c.document.variable_map) } docUpdateCallback?.invoke(docId, targetDoc?.c?.toSerializedBytes(Feedback)) setLiveDoc(targetDoc) @@ -503,6 +503,7 @@ internal fun DocServer.doc( // Don't return a doc with the wrong ID. if (liveDoc != null && liveDoc.c.docId == docId) return liveDoc if (preloadedDoc != null && preloadedDoc.c.docId == docId) { + VariableManager.init(preloadedDoc.c.docId.id, preloadedDoc.c.document.variable_map) docUpdateCallback?.invoke(docId, preloadedDoc.c.toSerializedBytes(Feedback)) endSection() return preloadedDoc @@ -518,6 +519,7 @@ internal fun DocServer.doc( synchronized(DesignSettings.fileFetchStatus) { DesignSettings.fileFetchStatus[docId]?.lastLoadFromDisk = Instant.now() } + VariableManager.init(decodedDoc.c.docId.id, decodedDoc.c.document.variable_map) docUpdateCallback?.invoke(docId, decodedDoc.c.toSerializedBytes(Feedback)) endSection() return decodedDoc diff --git a/designcompose/src/main/java/com/android/designcompose/FrameRender.kt b/designcompose/src/main/java/com/android/designcompose/FrameRender.kt index 8c68dd587..403402cf0 100644 --- a/designcompose/src/main/java/com/android/designcompose/FrameRender.kt +++ b/designcompose/src/main/java/com/android/designcompose/FrameRender.kt @@ -253,6 +253,7 @@ internal fun ContentDrawScope.render( name: String, customizations: CustomizationContext, layoutId: Int, + variableState: VariableState, ) { if (size.width <= 0F && size.height <= 0F) return @@ -343,7 +344,8 @@ internal fun ContentDrawScope.render( customArcAngle, vectorScaleX, vectorScaleY, - layoutId + layoutId, + variableState ) val customFillBrushFunction = customizations.getBrushFunction(name) @@ -362,7 +364,7 @@ internal fun ContentDrawScope.render( } else { style.node_style.background.mapNotNull { background -> val p = Paint() - val b = background.asBrush(document, density) + val b = background.asBrush(document, density, variableState) if (b != null) { val (brush, fillOpacity) = b val brushSize = getNodeRenderSize(rectSize, size, style, layoutId, density) @@ -377,7 +379,7 @@ internal fun ContentDrawScope.render( val strokeBrush = style.node_style.stroke.strokes.mapNotNull { background -> val p = Paint() - val b = background.asBrush(document, density) + val b = background.asBrush(document, density, variableState) if (b != null) { val (brush, strokeOpacity) = b val brushSize = getNodeRenderSize(rectSize, size, style, layoutId, density) @@ -514,6 +516,7 @@ internal fun squooshShapeRender( document: DocContent, name: String, customizations: CustomizationContext, + variableState: VariableState, drawContent: () -> Unit ) { if (size.width <= 0F && size.height <= 0F) return @@ -598,7 +601,8 @@ internal fun squooshShapeRender( customArcAngle, vectorScaleX, vectorScaleY, - 0 // XXX: layoutId + 0, // XXX: layoutId + variableState ) // Blend mode @@ -661,7 +665,7 @@ internal fun squooshShapeRender( } else { style.node_style.background.mapNotNull { background -> val p = Paint() - val b = background.asBrush(document, density) + val b = background.asBrush(document, density, variableState) if (b != null) { val (brush, fillOpacity) = b brush.applyTo(size, p, fillOpacity) @@ -674,7 +678,7 @@ internal fun squooshShapeRender( val strokeBrush = style.node_style.stroke.strokes.mapNotNull { background -> val p = Paint() - val b = background.asBrush(document, density) + val b = background.asBrush(document, density, variableState) if (b != null) { val (brush, strokeOpacity) = b brush.applyTo(size, p, strokeOpacity) diff --git a/designcompose/src/main/java/com/android/designcompose/MaterialThemeValues.kt b/designcompose/src/main/java/com/android/designcompose/MaterialThemeValues.kt new file mode 100644 index 000000000..4bef0789b --- /dev/null +++ b/designcompose/src/main/java/com/android/designcompose/MaterialThemeValues.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.designcompose + +import androidx.compose.ui.graphics.Color + +// Collection name for material theme generated from Material Theme Builder plugin +const val MATERIAL_THEME_COLLECTION_NAME = "material-theme" + +// Material theme variable names for variables generated from Material Theme Builder plugin +object MaterialThemeConstants { + const val PRIMARY = "Schemes/Primary" + const val ON_PRIMARY = "Schemes/On Primary" + const val PRIMARY_CONTAINER = "Schemes/Primary Container" + const val ON_PRIMARY_CONTAINER = "Schemes/On Primary Container" + const val INVERSE_PRIMARY = "Schemes/Inverse Primary" + const val SECONDARY = "Schemes/Secondary" + const val ON_SECONDARY = "Schemes/On Secondary" + const val SECONDARY_CONTAINER = "Schemes/Secondary Container" + const val ON_SECONDARY_CONTAINER = "Schemes/On Secondary Container" + const val TERTIARY = "Schemes/Tertiary" + const val ON_TERTIARY = "Schemes/On Tertiary" + const val TERTIARY_CONTAINER = "Schemes/Tertiary Container" + const val ON_TERTIARY_CONTAINER = "Schemes/On Tertiary Container" + const val BACKGROUND = "Schemes/Background" + const val ON_BACKGROUND = "Schemes/On Background" + const val SURFACE = "Schemes/Surface" + const val ON_SURFACE = "Schemes/On Surface" + const val SURFACE_VARIANT = "Schemes/Surface Variant" + const val ON_SURFACE_VARIANT = "Schemes/On Surface Variant" + const val SURFACE_TINT = "Schemes/Surface Tint" + const val INVERSE_SURFACE = "Schemes/Inverse Surface" + const val INVERSE_ON_SURFACE = "Schemes/Inverse On Surface" + const val ERROR = "Schemes/Error" + const val ON_ERROR = "Schemes/On Error" + const val ERROR_CONTAINER = "Schemes/Error Container" + const val ON_ERROR_CONTAINER = "Schemes/On Error Container" + const val OUTLINE = "Schemes/Outline" + const val OUTLINE_VARIANT = "Schemes/Outline Variant" + const val SCRIM = "Schemes/Scrim" + const val SURFACE_BRIGHT = "Schemes/Surface Bright" + const val SURFACE_DIM = "Schemes/Surface Dim" + const val SURFACE_CONTAINER = "Schemes/Surface Container" + const val SURFACE_CONTAINER_HIGH = "Schemes/Surface Container High" + const val SURFACE_CONTAINER_HIGHEST = "Schemes/Surface Container Highest" + const val SURFACE_CONTAINER_LOW = "Schemes/Surface Container Low" + const val SURFACE_CONTAINER_LOWEST = "Schemes/Surface Container Lowest" +} + +// Helper object to convert a variable name into the corresponding Material Theme value +internal object MaterialThemeValues { + fun getColor(name: String, collectionId: String, state: VariableState): Color? { + if (!state.useMaterialTheme) return null + val collection = VariableManager.getCollection(collectionId) + if (collection == null || collection.name != MATERIAL_THEME_COLLECTION_NAME) return null + + state.materialColorScheme?.let { c -> + return when (name) { + MaterialThemeConstants.PRIMARY -> c.primary + MaterialThemeConstants.ON_PRIMARY -> c.onPrimary + MaterialThemeConstants.PRIMARY_CONTAINER -> c.primaryContainer + MaterialThemeConstants.ON_PRIMARY_CONTAINER -> c.onPrimaryContainer + MaterialThemeConstants.INVERSE_PRIMARY -> c.inversePrimary + MaterialThemeConstants.SECONDARY -> c.secondary + MaterialThemeConstants.ON_SECONDARY -> c.onSecondary + MaterialThemeConstants.SECONDARY_CONTAINER -> c.secondaryContainer + MaterialThemeConstants.ON_SECONDARY_CONTAINER -> c.onSecondaryContainer + MaterialThemeConstants.TERTIARY -> c.tertiary + MaterialThemeConstants.ON_TERTIARY -> c.onTertiary + MaterialThemeConstants.TERTIARY_CONTAINER -> c.tertiaryContainer + MaterialThemeConstants.ON_TERTIARY_CONTAINER -> c.onTertiaryContainer + MaterialThemeConstants.BACKGROUND -> c.background + MaterialThemeConstants.ON_BACKGROUND -> c.onBackground + MaterialThemeConstants.SURFACE -> c.surface + MaterialThemeConstants.ON_SURFACE -> c.onSurface + MaterialThemeConstants.SURFACE_VARIANT -> c.surfaceVariant + MaterialThemeConstants.ON_SURFACE_VARIANT -> c.onSurfaceVariant + MaterialThemeConstants.SURFACE_TINT -> c.surfaceTint + MaterialThemeConstants.INVERSE_SURFACE -> c.inverseSurface + MaterialThemeConstants.INVERSE_ON_SURFACE -> c.inverseOnSurface + MaterialThemeConstants.ERROR -> c.error + MaterialThemeConstants.ON_ERROR -> c.onError + MaterialThemeConstants.ERROR_CONTAINER -> c.errorContainer + MaterialThemeConstants.ON_ERROR_CONTAINER -> c.onErrorContainer + MaterialThemeConstants.OUTLINE -> c.outline + MaterialThemeConstants.OUTLINE_VARIANT -> c.outlineVariant + MaterialThemeConstants.SCRIM -> c.scrim + MaterialThemeConstants.SURFACE_BRIGHT -> c.surfaceBright + MaterialThemeConstants.SURFACE_DIM -> c.surfaceDim + MaterialThemeConstants.SURFACE_CONTAINER -> c.surfaceContainer + MaterialThemeConstants.SURFACE_CONTAINER_HIGH -> c.surfaceContainerHigh + MaterialThemeConstants.SURFACE_CONTAINER_HIGHEST -> c.surfaceContainerHighest + MaterialThemeConstants.SURFACE_CONTAINER_LOW -> c.surfaceContainerLow + MaterialThemeConstants.SURFACE_CONTAINER_LOWEST -> c.surfaceContainerLowest + else -> null + } + } + return null + } +} diff --git a/designcompose/src/main/java/com/android/designcompose/Utils.kt b/designcompose/src/main/java/com/android/designcompose/Utils.kt index 577314a76..8b94e6df0 100644 --- a/designcompose/src/main/java/com/android/designcompose/Utils.kt +++ b/designcompose/src/main/java/com/android/designcompose/Utils.kt @@ -1203,10 +1203,15 @@ internal fun java.util.Optional>.asSkiaMatrix(): Matrix? { } /** Convert a Background to a Brush, returning a Pair of Brush and Opacity */ -internal fun Background.asBrush(document: DocContent, density: Float): Pair? { +internal fun Background.asBrush( + document: DocContent, + density: Float, + variableState: VariableState +): Pair? { when (this) { is Background.Solid -> { - return Pair(SolidColor(convertColor(value)), 1.0f) + val color = value.getValue(variableState) + return color?.let { Pair(SolidColor(color), 1.0f) } } is Background.Image -> { val backgroundImage = this @@ -1551,3 +1556,11 @@ internal fun Transition.asAnimationSpec(): AnimationSpec { else -> snap(0) } } + +internal fun com.android.designcompose.serdegen.Color.toColor(): Color { + val a = color[3].toInt() + val r = color[0].toInt() + val g = color[1].toInt() + val b = color[2].toInt() + return Color(r, g, b, a) +} diff --git a/designcompose/src/main/java/com/android/designcompose/VariableManager.kt b/designcompose/src/main/java/com/android/designcompose/VariableManager.kt new file mode 100644 index 000000000..d61806380 --- /dev/null +++ b/designcompose/src/main/java/com/android/designcompose/VariableManager.kt @@ -0,0 +1,294 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.designcompose + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.Color +import com.android.designcompose.serdegen.ColorOrVar +import com.android.designcompose.serdegen.NumOrVar +import com.android.designcompose.serdegen.Variable +import com.android.designcompose.serdegen.VariableMap +import com.android.designcompose.serdegen.VariableValue + +// A variable mode, e.g. "light" or "dark" +typealias VariableMode = String + +// A variable collection, e.g. "material-theme" or "my-theme" +typealias VarCollectionName = String + +// A mapping of collection names to the current mode for that collection +typealias VariableModeValues = HashMap + +// Current variable collection to use +private val LocalVariableCollection = compositionLocalOf { null } + +// Current set of variable modes to use, if overridden in code +private val LocalVariableModeValuesOverride = compositionLocalOf { null } + +// Current set of variable modes to use, as retrieved from the design source +private val LocalVariableModeValuesDoc = compositionLocalOf { hashMapOf() } + +// Current boolean value to represent whether we should attempt to override the design specified +// theme with the MaterialTheme +private val LocalVariableUseMaterialTheme = compositionLocalOf { false } + +// An accessor class to retrieve the current values of various variable states +object LocalVariableState { + val collection: VarCollectionName? + @Composable @ReadOnlyComposable get() = LocalVariableCollection.current + + val modeValues: VariableModeValues? + @Composable @ReadOnlyComposable get() = LocalVariableModeValuesOverride.current + + val useMaterialTheme: Boolean + @Composable @ReadOnlyComposable get() = LocalVariableUseMaterialTheme.current + + @Composable + internal fun hasOverrideModeValues(): Boolean { + return modeValues != null + } +} + +// This class takes a snapshot of the current state of all variable values, including the current +// collection, modes, and material theme values. Since the current values must be retrieved from +// a composable function but is needed by non-composable functions, we construct this snapshot +// and pass it to the non-composable functions +internal class VariableState( + val varCollection: VarCollectionName? = null, + val varModeValues: VariableModeValues? = null, + val useMaterialTheme: Boolean = false, + val materialColorScheme: ColorScheme? = null, + val materialTypography: Typography? = null, + val materialShapes: Shapes? = null, +) { + companion object { + @Composable + fun create(updatedModeValues: VariableModeValues? = null): VariableState { + return VariableState( + varCollection = LocalVariableState.collection, + varModeValues = updatedModeValues ?: LocalVariableState.modeValues, + useMaterialTheme = LocalVariableState.useMaterialTheme, + materialColorScheme = MaterialTheme.colorScheme, + materialTypography = MaterialTheme.typography, + materialShapes = MaterialTheme.shapes, + ) + } + } +} + +// Declare a CompositionLocal object of the specified variable collection. If null, no collection is +// set and whatever collection variables refer to will be used +@Composable +fun DesignVariableCollection(collection: VarCollectionName?, content: @Composable () -> Unit) = + if (collection != null) + CompositionLocalProvider(LocalVariableCollection provides collection) { content() } + else content() + +// Declare a CompositionLocal object of the specified mode values. If null, no mode is set and +// whatever mode values are set in the node are used, or the default mode of the collection. +@Composable +fun DesignVariableModeValues(modeValues: VariableModeValues?, content: @Composable () -> Unit) = + if (modeValues != null) + CompositionLocalProvider(LocalVariableModeValuesOverride provides modeValues) { content() } + else content() + +// Declare a CompositionLocal object to specify whether the current MaterialTheme should be looked +// up and used in place of variable values, if the variables are MaterialTheme variables. +@Composable +fun DesignMaterialThemeProvider(useMaterialTheme: Boolean = true, content: @Composable () -> Unit) = + CompositionLocalProvider(LocalVariableUseMaterialTheme provides useMaterialTheme) { content() } + +// VariableManager holds the VariableMap retrieved from the design file and has functions to find +// the value of a variable from the variable ID. +internal object VariableManager { + // Since we can have variables from multiple documents, store a variable map for each document + private val docVarMap: HashMap = HashMap() + // A global variable map containing entries from all documents. We currently don't support + // duplicate variable names across multiple documents. + private var varMap: VariableMap = VariableMap(HashMap(), HashMap(), HashMap(), HashMap()) + + internal fun init(docId: String, map: VariableMap) { + // Remove old entries for docId + val oldVarMap = docVarMap[docId] + oldVarMap?.collections?.forEach { varMap.collections.remove(it.key) } + oldVarMap?.collection_name_map?.forEach { varMap.collection_name_map.remove(it.key) } + oldVarMap?.variables?.forEach { varMap.variables.remove(it.key) } + oldVarMap?.variable_name_map?.forEach { varMap.variable_name_map.remove(it.key) } + + // Add new entries for docId + docVarMap[docId] = map + varMap.collections.putAll(map.collections) + varMap.collection_name_map.putAll(map.collection_name_map) + varMap.variables.putAll(map.variables) + varMap.variable_name_map.putAll(map.variable_name_map) + } + + // Given a set of explicitly set mode values on a node, create a VariableModeValues hash + // combining the current mode values with the values passed in. + @Composable + internal fun updateVariableStateFromModeValues( + modeValues: MutableMap + ): VariableModeValues { + val newModeValues = VariableModeValues(LocalVariableModeValuesDoc.current) + modeValues.forEach { (collectionId, modeId) -> + val collection = varMap.collections[collectionId] + collection?.let { c -> + val mode = c.mode_id_hash[modeId] + mode?.let { m -> newModeValues[c.name] = m.name } + } + } + return newModeValues + } + + // Return the collection given the collection ID + internal fun getCollection( + collectionId: String + ): com.android.designcompose.serdegen.Collection? { + return varMap.collections[collectionId] + } + + // Given a variable ID, return the color associated with it + internal fun getColor(varId: String, variableState: VariableState): Color? { + // Resolve varId into a variable. If a different collection has been set, this will return + // a variable of the same name from the override collection. + val variable = resolveVariable(varId, variableState) + variable?.let { v -> + return v.getColor(varMap, variableState) + } + return null + } + + // Given a variable ID, return the number associated with it + internal fun getNumber(varId: String, variableState: VariableState): Float? { + val variable = resolveVariable(varId, variableState) + variable?.let { v -> + return v.getNumber(varMap, variableState) + } + return null + } + + // Given a variable ID, return a Variable if it can be found. If an override collection has been + // set, this will return a variable from that collection if one of the same name exists. + // Otherwise, this will return the variable with the given ID. + private fun resolveVariable(varId: String, variableState: VariableState): Variable? { + val variable = varMap.variables[varId] + variable?.let { v -> + // If using material theme, return the variable since we don't need to resolve it based + // on an overridden collection + if (variableState.useMaterialTheme) return v + val collectionOverride = variableState.varCollection + collectionOverride?.let { cName -> + val collectionId = varMap.collection_name_map[cName] + collectionId?.let { cId -> + val nameMap = varMap.variable_name_map[cId] + nameMap?.let { nMap -> + val resolvedVarId = nMap[v.name] + resolvedVarId?.let { newVarId -> + return varMap.variables[newVarId] + } + } + } + } + return v + } + return null + } + + // Retrieve the VariableValue from this variable given the current variable state + private fun Variable.getValue( + variableMap: VariableMap, + variableState: VariableState + ): VariableValue? { + val collection = variableMap.collections[variable_collection_id] + collection?.let { c -> + val modeName = variableState.varModeValues?.get(c.name) + val modeId = + modeName?.let { mName -> c.mode_name_hash[mName] } + ?: c.mode_id_hash[c.default_mode_id] + ?.id // Fallback to default mode in collection + modeId?.let { mId -> + return values_by_mode.values_by_mode[mId] + } + } + return null + } + + // Return this variable's color given the current variable state. + private fun Variable.getColor( + variableMap: VariableMap, + variableState: VariableState, + ): Color? { + // Use the material theme override if one exists + MaterialThemeValues.getColor(name, variable_collection_id, variableState)?.let { + return it + } + val value = getValue(variableMap, variableState) + value?.let { vv -> + when (vv) { + is VariableValue.Color -> return vv.value.toColor() + is VariableValue.Alias -> + return resolveVariable(vv.value.id, variableState) + ?.getColor(variableMap, variableState) + else -> return null + } + } + return null + } + + // Return this variable's number given the current variable state. + private fun Variable.getNumber( + variableMap: VariableMap, + variableState: VariableState, + ): Float? { + val value = getValue(variableMap, variableState) + value?.let { vv -> + when (vv) { + is VariableValue.Number -> return vv.value + is VariableValue.Alias -> + return resolveVariable(vv.value.id, variableState) + ?.getNumber(variableMap, variableState) + else -> return null + } + } + return null + } +} + +// Return the value out of a NumOrVar enum. +internal fun NumOrVar.getValue(variableState: VariableState): Float { + return when (this) { + is NumOrVar.Num -> value + is NumOrVar.Var -> VariableManager.getNumber(value, variableState) ?: 0F + else -> 0F + } +} + +// Return the value of a ColorOrVar enum +internal fun ColorOrVar.getValue(variableState: VariableState): Color? { + return when (this) { + is ColorOrVar.Color -> value.toColor() + is ColorOrVar.Var -> VariableManager.getColor(value, variableState) + else -> null + } +} diff --git a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshAnimate.kt b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshAnimate.kt index 20c53c446..86f39db7d 100644 --- a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshAnimate.kt +++ b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshAnimate.kt @@ -110,6 +110,7 @@ internal class SquooshAnimatedArc( viewBuilder.frame_extras = target.view.frame_extras viewBuilder.reactions = target.view.reactions viewBuilder.render_method = target.view.render_method + viewBuilder.explicit_variable_modes = target.view.explicit_variable_modes viewBuilder.scroll_info = target.view.scroll_info viewBuilder.unique_id = target.view.unique_id val view = viewBuilder.build() diff --git a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRender.kt b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRender.kt index 723135b0b..c929b73f7 100644 --- a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRender.kt +++ b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRender.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.Density import com.android.designcompose.CustomizationContext import com.android.designcompose.DocContent import com.android.designcompose.TextMeasureData +import com.android.designcompose.VariableState import com.android.designcompose.asComposeBlendMode import com.android.designcompose.asComposeTransform import com.android.designcompose.getBrush @@ -61,7 +62,8 @@ internal fun Modifier.squooshRender( customizations: CustomizationContext, childRenderSelector: SquooshChildRenderSelector, animations: Map, - animationValues: State> + animationValues: State>, + variableState: VariableState, ): Modifier = this.then( Modifier.drawWithContent { @@ -137,7 +139,8 @@ internal fun Modifier.squooshRender( null, // customImageWithContext document, node.view.name, - customizations + customizations, + variableState, ) { var child = node.firstChild var pendingMask: SquooshResolvedNode? = null diff --git a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt index 06a6dc658..a02468e9e 100644 --- a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt +++ b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt @@ -55,6 +55,7 @@ import com.android.designcompose.DocumentSwitcher import com.android.designcompose.InteractionState import com.android.designcompose.InteractionStateManager import com.android.designcompose.LiveUpdateMode +import com.android.designcompose.VariableState import com.android.designcompose.asAnimationSpec import com.android.designcompose.asBuilder import com.android.designcompose.branches @@ -368,7 +369,8 @@ fun SquooshRoot( layoutIdAllocator, variantParentName, isRoot, - overlays + VariableState.create(), + overlays, ) ?: return val rootRemovalNodes = layoutIdAllocator.removalNodes() @@ -419,7 +421,8 @@ fun SquooshRoot( layoutIdAllocator, variantParentName, isRoot, - overlays + VariableState.create(), + overlays, ) transitionRootRemovalNodes = layoutIdAllocator.removalNodes() } @@ -456,6 +459,7 @@ fun SquooshRoot( // Is there a nicer way of passing these two? currentAnimations, animationValues, + VariableState.create(), ) .semantics { sDocRenderStatus = DocRenderStatus.Rendered }, measurePolicy = { measurables, constraints -> diff --git a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshText.kt b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshText.kt index f10a20805..5313ab982 100644 --- a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshText.kt +++ b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshText.kt @@ -36,6 +36,7 @@ import com.android.designcompose.CustomizationContext import com.android.designcompose.DesignSettings import com.android.designcompose.DocContent import com.android.designcompose.TextMeasureData +import com.android.designcompose.VariableState import com.android.designcompose.asBrush import com.android.designcompose.blurFudgeFactor import com.android.designcompose.convertColor @@ -67,7 +68,8 @@ internal fun squooshComputeTextInfo( density: Density, document: DocContent, customizations: CustomizationContext, - fontResourceLoader: Font.ResourceLoader + fontResourceLoader: Font.ResourceLoader, + variableState: VariableState, ): TextMeasureData? { val customizedText = customizations.getText(v.name) val customTextStyle = customizations.getTextStyle(v.name) @@ -89,7 +91,7 @@ internal fun squooshComputeTextInfo( val builder = AnnotatedString.Builder() for (run in (v.data as ViewData.StyledText).content) { val textBrushAndOpacity = - run.style.text_color.asBrush(document, density.density) + run.style.text_color.asBrush(document, density.density, variableState) builder.pushStyle( @OptIn(ExperimentalTextApi::class) (SpanStyle( @@ -153,7 +155,8 @@ internal fun squooshComputeTextInfo( ) ) } - val textBrushAndOpacity = v.style.node_style.text_color.asBrush(document, density.density) + val textBrushAndOpacity = + v.style.node_style.text_color.asBrush(document, density.density, variableState) val textStyle = @OptIn(ExperimentalTextApi::class) (TextStyle( diff --git a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshTreeBuilder.kt b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshTreeBuilder.kt index bb11d36bd..458bc2d8c 100644 --- a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshTreeBuilder.kt +++ b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshTreeBuilder.kt @@ -24,6 +24,7 @@ import com.android.designcompose.CustomizationContext import com.android.designcompose.DocContent import com.android.designcompose.InteractionState import com.android.designcompose.ReplacementContent +import com.android.designcompose.VariableState import com.android.designcompose.asBuilder import com.android.designcompose.getComponent import com.android.designcompose.getContent @@ -35,6 +36,7 @@ import com.android.designcompose.serdegen.Action import com.android.designcompose.serdegen.AlignItems import com.android.designcompose.serdegen.Background import com.android.designcompose.serdegen.Color +import com.android.designcompose.serdegen.ColorOrVar import com.android.designcompose.serdegen.ComponentInfo import com.android.designcompose.serdegen.Dimension import com.android.designcompose.serdegen.FlexDirection @@ -132,7 +134,8 @@ internal fun resolveVariantsRecursively( layoutIdAllocator: SquooshLayoutIdAllocator, variantParentName: String = "", isRoot: Boolean, - overlays: List? = null + variableState: VariableState, + overlays: List? = null, ): SquooshResolvedNode? { if (!customizations.getVisible(v.name)) return null var componentLayoutId = rootLayoutId @@ -215,7 +218,14 @@ internal fun resolveVariantsRecursively( // XXX-PERF: computeTextInfo is *super* slow. It needs to use a cache between frames. val textInfo = - squooshComputeTextInfo(view, density, document, customizations, fontResourceLoader) + squooshComputeTextInfo( + view, + density, + document, + customizations, + fontResourceLoader, + variableState + ) val resolvedView = SquooshResolvedNode(view, style, layoutId, textInfo, v.id) if (view.data is ViewData.Container) { @@ -238,6 +248,7 @@ internal fun resolveVariantsRecursively( layoutIdAllocator, "", false, + variableState, ) ?: continue childResolvedNode.parent = resolvedView @@ -316,7 +327,8 @@ internal fun resolveVariantsRecursively( composableList, layoutIdAllocator, "", - false + false, + variableState, ) ?: continue // Make a synthetic parent for the overlay. @@ -410,7 +422,7 @@ private fun generateOverlayNode( ) val colorBuilder = Color.Builder() colorBuilder.color = c - nodeStyle.background = listOf(Background.Solid(colorBuilder.build())) + nodeStyle.background = listOf(Background.Solid(ColorOrVar.Color(colorBuilder.build()))) } overlayStyle.layout_style = layoutStyle.build() overlayStyle.node_style = nodeStyle.build() @@ -449,6 +461,7 @@ private fun generateOverlayNode( overlayView.data = overlayViewData.build() overlayView.design_absolute_bounding_box = Optional.empty() overlayView.render_method = RenderMethod.None() + overlayView.explicit_variable_modes = Optional.empty() val layoutId = rootLayoutId + node.layoutId + 0x20000000 layoutIdAllocator.visitLayoutId(layoutId) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bbaca7699..bcd2a49ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,7 @@ truth = "1.4.2" spotless = "6.25.0" robolectric = "4.12.2" roborazzi = "1.17.0" +androidx-material3 = "1.2.1" # Keep in sync with the "Install Protoc" steps in .github/workflows protoc = "4.27.0" protobuf-plugin = "0.9.4" @@ -97,6 +98,7 @@ androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose-components" } androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "androidx-compose-components" } androidx-compose-material = { module = "androidx.compose.material:material" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidx-material3" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "androidx-compose-components" } androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "androidx-compose-components" } androidx-compose-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version.ref = "androidx-compose-runtime-tracing" } diff --git a/integration-tests/validation/build.gradle.kts b/integration-tests/validation/build.gradle.kts index 9f6af5791..9b7eacbcd 100644 --- a/integration-tests/validation/build.gradle.kts +++ b/integration-tests/validation/build.gradle.kts @@ -86,6 +86,7 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material) + implementation(libs.androidx.material3) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.material) implementation("androidx.media3:media3-exoplayer:1.3.1") diff --git a/integration-tests/validation/src/main/assets/figma/AlignmentTestDoc_JIjE9oKQbq8ipi66ab5UaK.dcf b/integration-tests/validation/src/main/assets/figma/AlignmentTestDoc_JIjE9oKQbq8ipi66ab5UaK.dcf index 54e3f72de..8c1def6e4 100644 Binary files a/integration-tests/validation/src/main/assets/figma/AlignmentTestDoc_JIjE9oKQbq8ipi66ab5UaK.dcf and b/integration-tests/validation/src/main/assets/figma/AlignmentTestDoc_JIjE9oKQbq8ipi66ab5UaK.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/BattleshipDoc_RfGl9SWnBEvdg8T1Ex6ZAR.dcf b/integration-tests/validation/src/main/assets/figma/BattleshipDoc_RfGl9SWnBEvdg8T1Ex6ZAR.dcf index 42c16d5f3..131c45b8e 100644 Binary files a/integration-tests/validation/src/main/assets/figma/BattleshipDoc_RfGl9SWnBEvdg8T1Ex6ZAR.dcf and b/integration-tests/validation/src/main/assets/figma/BattleshipDoc_RfGl9SWnBEvdg8T1Ex6ZAR.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/BlendModeTestDoc_ZqX5i5g6inv9tANIwMMXUV.dcf b/integration-tests/validation/src/main/assets/figma/BlendModeTestDoc_ZqX5i5g6inv9tANIwMMXUV.dcf index c7f367112..58eb25c9d 100644 Binary files a/integration-tests/validation/src/main/assets/figma/BlendModeTestDoc_ZqX5i5g6inv9tANIwMMXUV.dcf and b/integration-tests/validation/src/main/assets/figma/BlendModeTestDoc_ZqX5i5g6inv9tANIwMMXUV.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ColorTintTestDoc_MCtUD3yjONxK6rQm65yqM5.dcf b/integration-tests/validation/src/main/assets/figma/ColorTintTestDoc_MCtUD3yjONxK6rQm65yqM5.dcf index 650b759b0..26d1a63ec 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ColorTintTestDoc_MCtUD3yjONxK6rQm65yqM5.dcf and b/integration-tests/validation/src/main/assets/figma/ColorTintTestDoc_MCtUD3yjONxK6rQm65yqM5.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ComponentReplaceDoc_bQVVy2GSZJ8veYaJUrG6Ni.dcf b/integration-tests/validation/src/main/assets/figma/ComponentReplaceDoc_bQVVy2GSZJ8veYaJUrG6Ni.dcf index e626a0ca4..8a326acbb 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ComponentReplaceDoc_bQVVy2GSZJ8veYaJUrG6Ni.dcf and b/integration-tests/validation/src/main/assets/figma/ComponentReplaceDoc_bQVVy2GSZJ8veYaJUrG6Ni.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ConstraintsDoc_KuHLbsKA23DjZPhhgHqt71.dcf b/integration-tests/validation/src/main/assets/figma/ConstraintsDoc_KuHLbsKA23DjZPhhgHqt71.dcf index 32a3d6ba7..26e7ce64d 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ConstraintsDoc_KuHLbsKA23DjZPhhgHqt71.dcf and b/integration-tests/validation/src/main/assets/figma/ConstraintsDoc_KuHLbsKA23DjZPhhgHqt71.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/CrossAxisFillTestDoc_GPr1cx4n3zBPwLhqlSL1ba.dcf b/integration-tests/validation/src/main/assets/figma/CrossAxisFillTestDoc_GPr1cx4n3zBPwLhqlSL1ba.dcf index f0afbcf91..0cf2fa099 100644 Binary files a/integration-tests/validation/src/main/assets/figma/CrossAxisFillTestDoc_GPr1cx4n3zBPwLhqlSL1ba.dcf and b/integration-tests/validation/src/main/assets/figma/CrossAxisFillTestDoc_GPr1cx4n3zBPwLhqlSL1ba.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/CustomBrushTestDoc_oetCBVw8gCAxmCNllXx7zO.dcf b/integration-tests/validation/src/main/assets/figma/CustomBrushTestDoc_oetCBVw8gCAxmCNllXx7zO.dcf index 619a11568..6a4afc3aa 100644 Binary files a/integration-tests/validation/src/main/assets/figma/CustomBrushTestDoc_oetCBVw8gCAxmCNllXx7zO.dcf and b/integration-tests/validation/src/main/assets/figma/CustomBrushTestDoc_oetCBVw8gCAxmCNllXx7zO.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/DialsGaugesTestDoc_lZj6E9GtIQQE4HNLpzgETw.dcf b/integration-tests/validation/src/main/assets/figma/DialsGaugesTestDoc_lZj6E9GtIQQE4HNLpzgETw.dcf index fa9c6a20f..3fe6ecc71 100644 Binary files a/integration-tests/validation/src/main/assets/figma/DialsGaugesTestDoc_lZj6E9GtIQQE4HNLpzgETw.dcf and b/integration-tests/validation/src/main/assets/figma/DialsGaugesTestDoc_lZj6E9GtIQQE4HNLpzgETw.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/FancyFillTestDoc_xQ9cunHt8VUm6xqJJ2Pjb2.dcf b/integration-tests/validation/src/main/assets/figma/FancyFillTestDoc_xQ9cunHt8VUm6xqJJ2Pjb2.dcf index 114dfaa6e..f4f65705c 100644 Binary files a/integration-tests/validation/src/main/assets/figma/FancyFillTestDoc_xQ9cunHt8VUm6xqJJ2Pjb2.dcf and b/integration-tests/validation/src/main/assets/figma/FancyFillTestDoc_xQ9cunHt8VUm6xqJJ2Pjb2.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/FillTestDoc_dB3q96FkxkTO4czn5NqnxV.dcf b/integration-tests/validation/src/main/assets/figma/FillTestDoc_dB3q96FkxkTO4czn5NqnxV.dcf index fd1fc6403..fec36f3b5 100644 Binary files a/integration-tests/validation/src/main/assets/figma/FillTestDoc_dB3q96FkxkTO4czn5NqnxV.dcf and b/integration-tests/validation/src/main/assets/figma/FillTestDoc_dB3q96FkxkTO4czn5NqnxV.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/GridLayoutDoc_MBNjjSbzzKeN7nBjVoewsl.dcf b/integration-tests/validation/src/main/assets/figma/GridLayoutDoc_MBNjjSbzzKeN7nBjVoewsl.dcf index cf1c40f36..c7a49c084 100644 Binary files a/integration-tests/validation/src/main/assets/figma/GridLayoutDoc_MBNjjSbzzKeN7nBjVoewsl.dcf and b/integration-tests/validation/src/main/assets/figma/GridLayoutDoc_MBNjjSbzzKeN7nBjVoewsl.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/GridLayoutTestDoc_JOSOEvsrjvMqanyQa5OpNR.dcf b/integration-tests/validation/src/main/assets/figma/GridLayoutTestDoc_JOSOEvsrjvMqanyQa5OpNR.dcf index fd95afc3f..a527e51e4 100644 Binary files a/integration-tests/validation/src/main/assets/figma/GridLayoutTestDoc_JOSOEvsrjvMqanyQa5OpNR.dcf and b/integration-tests/validation/src/main/assets/figma/GridLayoutTestDoc_JOSOEvsrjvMqanyQa5OpNR.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/GridWidgetTestDoc_OBhNItd9i9J2LwVYuLxEIx.dcf b/integration-tests/validation/src/main/assets/figma/GridWidgetTestDoc_OBhNItd9i9J2LwVYuLxEIx.dcf index 691c5aba7..da3197418 100644 Binary files a/integration-tests/validation/src/main/assets/figma/GridWidgetTestDoc_OBhNItd9i9J2LwVYuLxEIx.dcf and b/integration-tests/validation/src/main/assets/figma/GridWidgetTestDoc_OBhNItd9i9J2LwVYuLxEIx.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/HelloByeDoc_MCHaMYcIEnRpbvU9Ms7a0o.dcf b/integration-tests/validation/src/main/assets/figma/HelloByeDoc_MCHaMYcIEnRpbvU9Ms7a0o.dcf index 1db063e7d..3e6d3fb60 100644 Binary files a/integration-tests/validation/src/main/assets/figma/HelloByeDoc_MCHaMYcIEnRpbvU9Ms7a0o.dcf and b/integration-tests/validation/src/main/assets/figma/HelloByeDoc_MCHaMYcIEnRpbvU9Ms7a0o.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/HelloVersionDoc_v62Vwlxa4Bb6nopJiAxQAQ_5668177823.dcf b/integration-tests/validation/src/main/assets/figma/HelloVersionDoc_v62Vwlxa4Bb6nopJiAxQAQ_5668177823.dcf index 0091bb67a..a4770fcea 100644 Binary files a/integration-tests/validation/src/main/assets/figma/HelloVersionDoc_v62Vwlxa4Bb6nopJiAxQAQ_5668177823.dcf and b/integration-tests/validation/src/main/assets/figma/HelloVersionDoc_v62Vwlxa4Bb6nopJiAxQAQ_5668177823.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf b/integration-tests/validation/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf index ccde07f44..3ec1da1f8 100644 Binary files a/integration-tests/validation/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf and b/integration-tests/validation/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ImageUpdateTestDoc_oQw7kiy94fvdVouCYBC9T0.dcf b/integration-tests/validation/src/main/assets/figma/ImageUpdateTestDoc_oQw7kiy94fvdVouCYBC9T0.dcf index 6c107325a..01b053248 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ImageUpdateTestDoc_oQw7kiy94fvdVouCYBC9T0.dcf and b/integration-tests/validation/src/main/assets/figma/ImageUpdateTestDoc_oQw7kiy94fvdVouCYBC9T0.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/InteractionTestDoc_8Zg9viyjYTnyN29pbkR1CE.dcf b/integration-tests/validation/src/main/assets/figma/InteractionTestDoc_8Zg9viyjYTnyN29pbkR1CE.dcf index a7eb451d5..c98274353 100644 Binary files a/integration-tests/validation/src/main/assets/figma/InteractionTestDoc_8Zg9viyjYTnyN29pbkR1CE.dcf and b/integration-tests/validation/src/main/assets/figma/InteractionTestDoc_8Zg9viyjYTnyN29pbkR1CE.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ItemSpacingTestDoc_YXrHBp6C6OaW5ShcCYeGJc.dcf b/integration-tests/validation/src/main/assets/figma/ItemSpacingTestDoc_YXrHBp6C6OaW5ShcCYeGJc.dcf index a121c0608..c8ee16a9a 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ItemSpacingTestDoc_YXrHBp6C6OaW5ShcCYeGJc.dcf and b/integration-tests/validation/src/main/assets/figma/ItemSpacingTestDoc_YXrHBp6C6OaW5ShcCYeGJc.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/LayoutReplacementTestDoc_dwk2GF7RiNvlbbAKPjqldx.dcf b/integration-tests/validation/src/main/assets/figma/LayoutReplacementTestDoc_dwk2GF7RiNvlbbAKPjqldx.dcf index babee8008..41752f214 100644 Binary files a/integration-tests/validation/src/main/assets/figma/LayoutReplacementTestDoc_dwk2GF7RiNvlbbAKPjqldx.dcf and b/integration-tests/validation/src/main/assets/figma/LayoutReplacementTestDoc_dwk2GF7RiNvlbbAKPjqldx.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/LayoutTestsDoc_Gv63fYTzpeH2ZtxP4go31E.dcf b/integration-tests/validation/src/main/assets/figma/LayoutTestsDoc_Gv63fYTzpeH2ZtxP4go31E.dcf index 2e782d339..b524c4c14 100644 Binary files a/integration-tests/validation/src/main/assets/figma/LayoutTestsDoc_Gv63fYTzpeH2ZtxP4go31E.dcf and b/integration-tests/validation/src/main/assets/figma/LayoutTestsDoc_Gv63fYTzpeH2ZtxP4go31E.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ListWidgetTestDoc_9ev0MBNHFrgTqJOrAGcEpV.dcf b/integration-tests/validation/src/main/assets/figma/ListWidgetTestDoc_9ev0MBNHFrgTqJOrAGcEpV.dcf index 207e4f30b..f0ba80377 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ListWidgetTestDoc_9ev0MBNHFrgTqJOrAGcEpV.dcf and b/integration-tests/validation/src/main/assets/figma/ListWidgetTestDoc_9ev0MBNHFrgTqJOrAGcEpV.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/MaskTestDoc_mEmdUVEIjvBBbV0kELPy37.dcf b/integration-tests/validation/src/main/assets/figma/MaskTestDoc_mEmdUVEIjvBBbV0kELPy37.dcf index 069f32c83..15afb6531 100644 Binary files a/integration-tests/validation/src/main/assets/figma/MaskTestDoc_mEmdUVEIjvBBbV0kELPy37.dcf and b/integration-tests/validation/src/main/assets/figma/MaskTestDoc_mEmdUVEIjvBBbV0kELPy37.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ModuleExampleDoc_hPEGkrF0LUqNYEZObXqjXZ.dcf b/integration-tests/validation/src/main/assets/figma/ModuleExampleDoc_hPEGkrF0LUqNYEZObXqjXZ.dcf index 9fcffef2a..cb461cfa9 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ModuleExampleDoc_hPEGkrF0LUqNYEZObXqjXZ.dcf and b/integration-tests/validation/src/main/assets/figma/ModuleExampleDoc_hPEGkrF0LUqNYEZObXqjXZ.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/OnePxSeparatorDoc_EXjTHxfMNBtXDrz8hr6MFB.dcf b/integration-tests/validation/src/main/assets/figma/OnePxSeparatorDoc_EXjTHxfMNBtXDrz8hr6MFB.dcf index d2b7b136f..66152b2e0 100644 Binary files a/integration-tests/validation/src/main/assets/figma/OnePxSeparatorDoc_EXjTHxfMNBtXDrz8hr6MFB.dcf and b/integration-tests/validation/src/main/assets/figma/OnePxSeparatorDoc_EXjTHxfMNBtXDrz8hr6MFB.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/OpenLinkTestDoc_r7m4tqyKv6y9DWcg7QBEDf.dcf b/integration-tests/validation/src/main/assets/figma/OpenLinkTestDoc_r7m4tqyKv6y9DWcg7QBEDf.dcf index c4d3d0e03..86446f12b 100644 Binary files a/integration-tests/validation/src/main/assets/figma/OpenLinkTestDoc_r7m4tqyKv6y9DWcg7QBEDf.dcf and b/integration-tests/validation/src/main/assets/figma/OpenLinkTestDoc_r7m4tqyKv6y9DWcg7QBEDf.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/RecursiveCustomizationsDoc_o0GWzcqdOWEgzj4kIeIlAu.dcf b/integration-tests/validation/src/main/assets/figma/RecursiveCustomizationsDoc_o0GWzcqdOWEgzj4kIeIlAu.dcf index 72185b98f..652108924 100644 Binary files a/integration-tests/validation/src/main/assets/figma/RecursiveCustomizationsDoc_o0GWzcqdOWEgzj4kIeIlAu.dcf and b/integration-tests/validation/src/main/assets/figma/RecursiveCustomizationsDoc_o0GWzcqdOWEgzj4kIeIlAu.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/ShadowsTestDoc_OqK58Y46IqP4wIgKCWys48.dcf b/integration-tests/validation/src/main/assets/figma/ShadowsTestDoc_OqK58Y46IqP4wIgKCWys48.dcf index d367a47c4..d7f7d8feb 100644 Binary files a/integration-tests/validation/src/main/assets/figma/ShadowsTestDoc_OqK58Y46IqP4wIgKCWys48.dcf and b/integration-tests/validation/src/main/assets/figma/ShadowsTestDoc_OqK58Y46IqP4wIgKCWys48.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/SmartAnimateTestDoc_RW3lFurXCoVDeqY2Y7bf4v.dcf b/integration-tests/validation/src/main/assets/figma/SmartAnimateTestDoc_RW3lFurXCoVDeqY2Y7bf4v.dcf index cb1085660..e6ca460b4 100644 Binary files a/integration-tests/validation/src/main/assets/figma/SmartAnimateTestDoc_RW3lFurXCoVDeqY2Y7bf4v.dcf and b/integration-tests/validation/src/main/assets/figma/SmartAnimateTestDoc_RW3lFurXCoVDeqY2Y7bf4v.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/TelltaleTestDoc_TZgHrKWx8wvQM7UPTyEpmz.dcf b/integration-tests/validation/src/main/assets/figma/TelltaleTestDoc_TZgHrKWx8wvQM7UPTyEpmz.dcf index 6c229e383..a8b070ee0 100644 Binary files a/integration-tests/validation/src/main/assets/figma/TelltaleTestDoc_TZgHrKWx8wvQM7UPTyEpmz.dcf and b/integration-tests/validation/src/main/assets/figma/TelltaleTestDoc_TZgHrKWx8wvQM7UPTyEpmz.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/TextElideTestDoc_oQ7nK49Ya5PJ3GpjI5iy8d.dcf b/integration-tests/validation/src/main/assets/figma/TextElideTestDoc_oQ7nK49Ya5PJ3GpjI5iy8d.dcf index a0094caac..4a85f26e7 100644 Binary files a/integration-tests/validation/src/main/assets/figma/TextElideTestDoc_oQ7nK49Ya5PJ3GpjI5iy8d.dcf and b/integration-tests/validation/src/main/assets/figma/TextElideTestDoc_oQ7nK49Ya5PJ3GpjI5iy8d.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/TextResizingTestDoc_kAoYvgHkPzA4J4pALZ3Xhg.dcf b/integration-tests/validation/src/main/assets/figma/TextResizingTestDoc_kAoYvgHkPzA4J4pALZ3Xhg.dcf index 7c157c65c..ae1a2e771 100644 Binary files a/integration-tests/validation/src/main/assets/figma/TextResizingTestDoc_kAoYvgHkPzA4J4pALZ3Xhg.dcf and b/integration-tests/validation/src/main/assets/figma/TextResizingTestDoc_kAoYvgHkPzA4J4pALZ3Xhg.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/VariableBorderTestDoc_MWnVAfW3FupV4VMLNR1m67.dcf b/integration-tests/validation/src/main/assets/figma/VariableBorderTestDoc_MWnVAfW3FupV4VMLNR1m67.dcf index ad477a433..253d5a6f8 100644 Binary files a/integration-tests/validation/src/main/assets/figma/VariableBorderTestDoc_MWnVAfW3FupV4VMLNR1m67.dcf and b/integration-tests/validation/src/main/assets/figma/VariableBorderTestDoc_MWnVAfW3FupV4VMLNR1m67.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/VariablesTestDoc_HhGxvL4aHhP8ALsLNz56TP.dcf b/integration-tests/validation/src/main/assets/figma/VariablesTestDoc_HhGxvL4aHhP8ALsLNz56TP.dcf new file mode 100644 index 000000000..04d2b7c6d Binary files /dev/null and b/integration-tests/validation/src/main/assets/figma/VariablesTestDoc_HhGxvL4aHhP8ALsLNz56TP.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/VariantAnimationTestDoc_pghyUUhlzJNoxxSK86ngiw.dcf b/integration-tests/validation/src/main/assets/figma/VariantAnimationTestDoc_pghyUUhlzJNoxxSK86ngiw.dcf index dfe7ba157..0582d8331 100644 Binary files a/integration-tests/validation/src/main/assets/figma/VariantAnimationTestDoc_pghyUUhlzJNoxxSK86ngiw.dcf and b/integration-tests/validation/src/main/assets/figma/VariantAnimationTestDoc_pghyUUhlzJNoxxSK86ngiw.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/VariantAsteriskTestDoc_gQeYHGCSaBE4zYSFpBrhre.dcf b/integration-tests/validation/src/main/assets/figma/VariantAsteriskTestDoc_gQeYHGCSaBE4zYSFpBrhre.dcf index b17a10f5e..d2b85f45d 100644 Binary files a/integration-tests/validation/src/main/assets/figma/VariantAsteriskTestDoc_gQeYHGCSaBE4zYSFpBrhre.dcf and b/integration-tests/validation/src/main/assets/figma/VariantAsteriskTestDoc_gQeYHGCSaBE4zYSFpBrhre.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/VariantInteractionsTestDoc_WcsgoLR4aDRSkZHY29Qdhq.dcf b/integration-tests/validation/src/main/assets/figma/VariantInteractionsTestDoc_WcsgoLR4aDRSkZHY29Qdhq.dcf index 2dee72dba..098995d59 100644 Binary files a/integration-tests/validation/src/main/assets/figma/VariantInteractionsTestDoc_WcsgoLR4aDRSkZHY29Qdhq.dcf and b/integration-tests/validation/src/main/assets/figma/VariantInteractionsTestDoc_WcsgoLR4aDRSkZHY29Qdhq.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/VariantPropertiesTestDoc_4P7zDdrQxj7FZsKJoIQcx1.dcf b/integration-tests/validation/src/main/assets/figma/VariantPropertiesTestDoc_4P7zDdrQxj7FZsKJoIQcx1.dcf index 2025a4803..1ad9b007f 100644 Binary files a/integration-tests/validation/src/main/assets/figma/VariantPropertiesTestDoc_4P7zDdrQxj7FZsKJoIQcx1.dcf and b/integration-tests/validation/src/main/assets/figma/VariantPropertiesTestDoc_4P7zDdrQxj7FZsKJoIQcx1.dcf differ diff --git a/integration-tests/validation/src/main/assets/figma/VectorRenderingTestDoc_Z3ucY0wMAbIwZIa6mLEWIK.dcf b/integration-tests/validation/src/main/assets/figma/VectorRenderingTestDoc_Z3ucY0wMAbIwZIa6mLEWIK.dcf index eb0d079de..09ad1f9ee 100644 Binary files a/integration-tests/validation/src/main/assets/figma/VectorRenderingTestDoc_Z3ucY0wMAbIwZIa6mLEWIK.dcf and b/integration-tests/validation/src/main/assets/figma/VectorRenderingTestDoc_Z3ucY0wMAbIwZIa6mLEWIK.dcf differ diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/LayoutTest.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/LayoutTest.kt index c67c97442..4409fe9e9 100644 --- a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/LayoutTest.kt +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/LayoutTest.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.ParentDataModifier import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -312,6 +313,25 @@ internal fun Button(name: String, selected: Boolean, select: () -> Unit) { } } +@Composable +internal fun TestButton(name: String, tag: String, selected: Boolean, select: () -> Unit) { + val textColor = if (selected) Color.Black else Color.Gray + val borderColor = if (selected) Color.Black else Color.Gray + var modifier = + Modifier.padding(10.dp) + .clickable { select() } + .border(width = 2.dp, color = borderColor, shape = RoundedCornerShape(8.dp)) + .absolutePadding(10.dp, 2.dp, 10.dp, 2.dp) + .testTag(tag) + + Box( + contentAlignment = Alignment.Center, + modifier = modifier, + ) { + Text(name, fontSize = 30.sp, color = textColor) + } +} + @Composable fun LayoutComposable() { val (showRect1, setShowRect1) = remember { mutableStateOf(true) } diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt index 53f9d8f04..4c49435bd 100644 --- a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt @@ -92,6 +92,7 @@ val EXAMPLES: ArrayList Unit, String?>> = // CompositingViewsTestDoc.javaClass.name), Triple("Text Inval", { TextResizingTest() }, TextResizingTestDoc.javaClass.name), Triple("Shared Customization", { ModuleExample() }, ModuleExampleDoc.javaClass.name), + Triple("Variable Modes", { VariableModesTest() }, VariablesTestDoc.javaClass.name), // GH-636: Test takes too long to execute. // Triple("Very large File", { VeryLargeFile() }, VeryLargeFileDoc.javaClass.name) ) diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariableModesTest.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariableModesTest.kt new file mode 100644 index 000000000..d34eeba27 --- /dev/null +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariableModesTest.kt @@ -0,0 +1,268 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.designcompose.testapp.validation.examples + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.designcompose.ComponentReplacementContext +import com.android.designcompose.DesignMaterialThemeProvider +import com.android.designcompose.DesignVariableCollection +import com.android.designcompose.DesignVariableModeValues +import com.android.designcompose.annotation.Design +import com.android.designcompose.annotation.DesignComponent +import com.android.designcompose.annotation.DesignDoc + +enum class LightDarkMode { + Default, + Light, + Dark, +} + +enum class Theme(val themeName: String) { + Material("material-theme"), + MyTheme("my-theme"), +} + +@DesignDoc(id = "HhGxvL4aHhP8ALsLNz56TP") +interface VariablesTest { + @DesignComponent(node = "#stage") + fun Main( + @Design(node = "#TopRight") topRight: @Composable (ComponentReplacementContext) -> Unit, + @Design(node = "#BottomRight") + bottomRight: @Composable (ComponentReplacementContext) -> Unit, + ) + + @DesignComponent(node = "#Box") fun Box(@Design(node = "#name") name: String) +} + +@Composable +fun VariableModesTest() { + // The variable theme (collection) override to use from Figma + val theme = remember { mutableStateOf(null) } + // The mode override + val mode = remember { mutableStateOf(LightDarkMode.Default) } + // If true, override any variable using material theme with the device's material theme + val useMaterialOverride = remember { mutableStateOf(false) } + // Top right node theme override + val trTheme = remember { mutableStateOf(null) } + // Top right node mode override + val trMode = remember { mutableStateOf(LightDarkMode.Default) } + // Bottom right node theme override + val brTheme = remember { mutableStateOf(null) } + // Bottom right node mode override + val brMode = remember { mutableStateOf(LightDarkMode.Default) } + + val themeName = theme.value?.themeName + val modeValues = + if (themeName != null && mode.value != LightDarkMode.Default) + hashMapOf(themeName to mode.value.name) + else null + + val trThemeName = trTheme.value?.themeName + val trModeValues = + if (trThemeName != null && trMode.value != LightDarkMode.Default) + hashMapOf(trThemeName to trMode.value.name) + else null + + val brThemeName = brTheme.value?.themeName + val brModeValues = + if (brThemeName != null && brMode.value != LightDarkMode.Default) + hashMapOf(brThemeName to brMode.value.name) + else null + + DesignVariableCollection(themeName) { + DesignVariableModeValues(modeValues) { + DesignMaterialThemeProvider(useMaterialTheme = useMaterialOverride.value) { + VariablesTestDoc.Main( + topRight = { + DesignVariableCollection(trThemeName) { + DesignVariableModeValues(trModeValues) { + VariablesTestDoc.Box(name = "Top Right") + } + } + }, + bottomRight = { + DesignVariableCollection(brThemeName) { + DesignVariableModeValues(brModeValues) { + VariablesTestDoc.Box(name = "Bottom Right") + } + } + }, + ) + } + } + } + + Column(Modifier.offset(10.dp, 800.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Root Theme", fontSize = 30.sp, color = Color.Black) + com.android.designcompose.testapp.validation.TestButton( + "None", + "RootThemeNone", + theme.value == null + ) { + theme.value = null + useMaterialOverride.value = false + } + com.android.designcompose.testapp.validation.TestButton( + "Material (Figma)", + "MaterialFigma", + theme.value == Theme.Material + ) { + theme.value = Theme.Material + useMaterialOverride.value = false + } + com.android.designcompose.testapp.validation.TestButton( + "MyTheme (Figma)", + "MyThemeFigma", + theme.value == Theme.MyTheme + ) { + theme.value = Theme.MyTheme + useMaterialOverride.value = false + } + com.android.designcompose.testapp.validation.TestButton( + "Material (Device)", + "MaterialDevice", + useMaterialOverride.value + ) { + theme.value = null + useMaterialOverride.value = true + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Root Mode", fontSize = 30.sp, color = Color.Black) + com.android.designcompose.testapp.validation.TestButton( + "Default", + "RootModeDefault", + mode.value == LightDarkMode.Default + ) { + mode.value = LightDarkMode.Default + } + com.android.designcompose.testapp.validation.TestButton( + "Light", + "RootModeLight", + mode.value == LightDarkMode.Light + ) { + mode.value = LightDarkMode.Light + } + com.android.designcompose.testapp.validation.TestButton( + "Dark", + "RootModeDark", + mode.value == LightDarkMode.Dark + ) { + mode.value = LightDarkMode.Dark + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Top Right Theme", fontSize = 30.sp, color = Color.Black) + com.android.designcompose.testapp.validation.TestButton( + "None", + "TopRightNone", + trTheme.value == null + ) { + trTheme.value = null + } + com.android.designcompose.testapp.validation.TestButton( + "Material", + "TopRightMaterial", + trTheme.value == Theme.Material + ) { + trTheme.value = Theme.Material + } + com.android.designcompose.testapp.validation.TestButton( + "MyTheme", + "TopRightMyTheme", + trTheme.value == Theme.MyTheme + ) { + trTheme.value = Theme.MyTheme + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Top Right Mode", fontSize = 30.sp, color = Color.Black) + com.android.designcompose.testapp.validation.TestButton( + "Default", + "TopRightDefault", + trMode.value == LightDarkMode.Default + ) { + trMode.value = LightDarkMode.Default + } + com.android.designcompose.testapp.validation.TestButton( + "Light", + "TopRightLight", + trMode.value == LightDarkMode.Light + ) { + trMode.value = LightDarkMode.Light + } + com.android.designcompose.testapp.validation.TestButton( + "Dark", + "TopRightDark", + trMode.value == LightDarkMode.Dark + ) { + trMode.value = LightDarkMode.Dark + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Bottom Right Theme", fontSize = 30.sp, color = Color.Black) + com.android.designcompose.testapp.validation.Button("None", brTheme.value == null) { + brTheme.value = null + } + com.android.designcompose.testapp.validation.Button( + "Material", + brTheme.value == Theme.Material + ) { + brTheme.value = Theme.Material + } + com.android.designcompose.testapp.validation.Button( + "MyTheme", + brTheme.value == Theme.MyTheme + ) { + brTheme.value = Theme.MyTheme + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Bottom Right Mode", fontSize = 30.sp, color = Color.Black) + com.android.designcompose.testapp.validation.Button( + "Default", + brMode.value == LightDarkMode.Default + ) { + brMode.value = LightDarkMode.Default + } + com.android.designcompose.testapp.validation.Button( + "Light", + brMode.value == LightDarkMode.Light + ) { + brMode.value = LightDarkMode.Light + } + com.android.designcompose.testapp.validation.Button( + "Dark", + brMode.value == LightDarkMode.Dark + ) { + brMode.value = LightDarkMode.Dark + } + } + } +} diff --git a/integration-tests/validation/src/testDebug/kotlin/com/android/designcompose/testapp/validation/VariableThemesModes.kt b/integration-tests/validation/src/testDebug/kotlin/com/android/designcompose/testapp/validation/VariableThemesModes.kt new file mode 100644 index 000000000..04d6b0b7f --- /dev/null +++ b/integration-tests/validation/src/testDebug/kotlin/com/android/designcompose/testapp/validation/VariableThemesModes.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.designcompose.testapp.validation + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.designcompose.test.internal.captureRootRoboImage +import com.android.designcompose.test.internal.designComposeRoborazziRule +import com.android.designcompose.testapp.validation.examples.VariableModesTest +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(qualifiers = RobolectricDeviceQualifiers.MediumTablet, sdk = [34]) +class VariableThemesModes { + @get:Rule val composeTestRule = createAndroidComposeRule() + @get:Rule val roborazziRule = designComposeRoborazziRule(javaClass.simpleName) + + @Before + fun setup() { + with(composeTestRule) { setContent { VariableModesTest() } } + } + + @Test + fun materialThemeFigmaTest() { + with(composeTestRule) { + onNodeWithTag("MaterialFigma").performClick() + captureRootRoboImage("MaterialFigma") + + onNodeWithTag("RootModeDark").performClick() + captureRootRoboImage("MaterialFigmaDark") + } + } + + @Test + fun myThemeFigmaTest() { + with(composeTestRule) { + onNodeWithTag("MyThemeFigma").performClick() + captureRootRoboImage("MyThemeFigma") + + onNodeWithTag("RootModeDark").performClick() + captureRootRoboImage("MyThemeFigmaDark") + } + } + + @Test + fun materialThemeDeviceTest() { + with(composeTestRule) { + onNodeWithTag("MaterialDevice").performClick() + captureRootRoboImage("MaterialDevice") + } + } + + @Test + fun replacementNodeThemeOverrideTest() { + with(composeTestRule) { + onNodeWithTag("RootThemeNone").performClick() + onNodeWithTag("TopRightMyTheme").performClick() + captureRootRoboImage("TopRightMyTheme") + + onNodeWithTag("TopRightDark").performClick() + captureRootRoboImage("TopRightDark") + } + } +} diff --git a/integration-tests/validation/src/testDebug/roborazzi/RenderAllExamples/Variable-Modes.png b/integration-tests/validation/src/testDebug/roborazzi/RenderAllExamples/Variable-Modes.png new file mode 100644 index 000000000..215c42b17 Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/RenderAllExamples/Variable-Modes.png differ diff --git a/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialDevice.png b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialDevice.png new file mode 100644 index 000000000..8a7ff0968 Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialDevice.png differ diff --git a/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialFigma.png b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialFigma.png new file mode 100644 index 000000000..a3da9ae2d Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialFigma.png differ diff --git a/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialFigmaDark.png b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialFigmaDark.png new file mode 100644 index 000000000..bdfa92d96 Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MaterialFigmaDark.png differ diff --git a/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MyThemeFigma.png b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MyThemeFigma.png new file mode 100644 index 000000000..8098e06bf Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MyThemeFigma.png differ diff --git a/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MyThemeFigmaDark.png b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MyThemeFigmaDark.png new file mode 100644 index 000000000..475095cde Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/MyThemeFigmaDark.png differ diff --git a/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/TopRightDark.png b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/TopRightDark.png new file mode 100644 index 000000000..90bd83641 Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/TopRightDark.png differ diff --git a/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/TopRightMyTheme.png b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/TopRightMyTheme.png new file mode 100644 index 000000000..ab885e13d Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/VariableThemesModes/TopRightMyTheme.png differ diff --git a/reference-apps/helloworld/helloworld-app/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf b/reference-apps/helloworld/helloworld-app/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf index 8af200269..7bc747efc 100644 Binary files a/reference-apps/helloworld/helloworld-app/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf and b/reference-apps/helloworld/helloworld-app/src/main/assets/figma/HelloWorldDoc_pxVlixodJqZL95zo2RzTHl.dcf differ diff --git a/reference-apps/tutorial/app/src/main/assets/figma/TutorialDoc_3z4xExq0INrL9vxPhj9tl7.dcf b/reference-apps/tutorial/app/src/main/assets/figma/TutorialDoc_3z4xExq0INrL9vxPhj9tl7.dcf index 23a13a202..0e67bae29 100644 Binary files a/reference-apps/tutorial/app/src/main/assets/figma/TutorialDoc_3z4xExq0INrL9vxPhj9tl7.dcf and b/reference-apps/tutorial/app/src/main/assets/figma/TutorialDoc_3z4xExq0INrL9vxPhj9tl7.dcf differ diff --git a/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt b/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt index 792d2af92..62c8efde0 100644 --- a/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt +++ b/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt @@ -78,6 +78,7 @@ enum class ButtonState { pressed, } +// Welcome branch BX9UyUa5lkuSP3dEnqBdJf @DesignDoc(id = "3z4xExq0INrL9vxPhj9tl7", customizationInterfaceVersion = "0.8") interface Tutorial { @DesignComponent(node = "#stage", isRoot = true)