Skip to content

Commit

Permalink
Closes #951: Initial support for variables and design tokens (#1055)
Browse files Browse the repository at this point in the history
Use the Figma API to parse variables. There are three main areas we now
parse:
1. boundVariables from nodes that use variables. This change supports
background color, stroke color, text color, and rounded corner sizes.
2. explicitVariableModes from nodes where Figma set a mode to be used on
that node and children.
3. All the variable data (collections, modes, variables) from a new
variables API call.

boundVariables and explicitVariableModes are stored in the view
generated for each node. The rest of the variable data is stored in a
variable map data structure stored in the serialized file.

Three new functions are introduced to override the current theme or
mode.
1. DesignVariableCollection(themeName) declares a
CompositionLocalProvider object that changes the current theme
(collection in Figma) to the specified theme name. This theme name must
exist as a collection in the Figma file, or it will do nothing.
2. DesignVariableModeValues(modeValues) declares a
CompositionLocalProvider hash map that maps collection names to mode
names. This changes the mode for the specified collection. For example
if the default mode in Figma is set to 'light', this can be used to
change the mode to 'dark'.
3. DesignMaterialThemeProvider(Boolean) declares a
CompositionLocalProvider boolean to specify whether the material theme
on the device should be used to override the theme in Figma. This only
works for variables that were created from the Material Theme Builder
plugin, or were made in a way to match -- e.g. the collection name is
"material-theme" and the variable names match the Material Theme names
such as "Schemes/Primary".

When it is time to render a node that uses variables, we need to
translate the variable into a value. This is achieved by using the
helper class VariableManager, which takes the current VariableState
containing the current collection and mode, along with the variable map
in the serialized file, and looks up the correct value for the variable.
  • Loading branch information
rylin8 authored May 30, 2024
1 parent dd94fcc commit 86a5fb8
Show file tree
Hide file tree
Showing 85 changed files with 1,445 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions crates/figma_import/src/bin/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions crates/figma_import/src/bin/fetch_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
71 changes: 69 additions & 2 deletions crates/figma_import/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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<Node>,
component_sets: HashMap<String, String>,
Expand Down Expand Up @@ -183,20 +187,34 @@ 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(),
document_id,
version_id,
proxy_config: proxy_config.clone(),
document_root,
variables_response,
image_context,
variant_nodes: vec![],
component_sets: HashMap::new(),
branches,
})
}

// 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<VariablesResponse, Error> {
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(
Expand Down Expand Up @@ -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<String, Collection> = HashMap::new();
let mut collection_name_map: HashMap<String, String> = HashMap::new();
for (_, c) in self.variables_response.meta.variable_collections.iter() {
let mut mode_name_hash: HashMap<String, String> = HashMap::new();
let mut mode_id_hash: HashMap<String, Mode> = 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<String, Variable> = HashMap::new();
let mut variable_name_map: HashMap<String, HashMap<String, String>> = 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 {
Expand Down
3 changes: 3 additions & 0 deletions crates/figma_import/src/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down
171 changes: 171 additions & 0 deletions crates/figma_import/src/figma_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -33,6 +34,11 @@ pub struct FigmaColor {
pub b: f32,
pub a: f32,
}
impl Into<Color> 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")]
Expand Down Expand Up @@ -472,6 +478,8 @@ pub struct Gradient {
pub enum PaintData {
Solid {
color: FigmaColor,
#[serde(rename = "boundVariables")]
bound_variables: Option<BoundVariables>,
},
GradientLinear {
#[serde(flatten)]
Expand Down Expand Up @@ -904,6 +912,8 @@ pub struct Node {
pub stroke_geometry: Option<Vec<Path>>,
#[serde(default = "default_stroke_cap")]
pub stroke_cap: StrokeCap,
pub bound_variables: Option<BoundVariables>,
pub explicit_variable_modes: Option<HashMap<String, String>>,
}

impl Node {
Expand Down Expand Up @@ -1070,3 +1080,164 @@ pub struct ProjectFilesResponse {
pub name: String,
pub files: Vec<HashMap<String, String>>,
}

#[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<ModeBinding>,
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<VariableAlias>),
}
impl VariableAliasOrList {
fn get_name(&self) -> Option<String> {
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<String, VariableAliasOrList>,
}
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<String> {
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<String, VariableValue>,
},
#[serde(rename_all = "camelCase")]
Float {
#[serde(flatten)]
common: VariableCommon,
values_by_mode: HashMap<String, VariableValue>,
},
#[serde(rename_all = "camelCase")]
String {
#[serde(flatten)]
common: VariableCommon,
values_by_mode: HashMap<String, VariableValue>,
},
#[serde(rename_all = "camelCase")]
Color {
#[serde(flatten)]
common: VariableCommon,
//#[serde(deserialize_with = "value_or_alias")]
values_by_mode: HashMap<String, VariableValue>,
},
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct VariablesMeta {
pub variable_collections: HashMap<String, VariableCollection>,
pub variables: HashMap<String, Variable>,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct VariablesResponse {
pub error: bool,
pub status: i32,
pub meta: VariablesMeta,
}
Loading

0 comments on commit 86a5fb8

Please sign in to comment.