mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 23:01:29 +03:00
Virtual Component Groups in the Hierarchical Action List (1/2) (#3488)
Parse the Engine's response containing Virtual Component Groups and store the results in a field of the Execution Context type. https://www.pivotaltracker.com/story/show/181865548 # Important Notes - This PR implements the subtask 1 of 2 in the ["Virtual Component Groups in the Hierarchical Action List" task](https://www.pivotaltracker.com/story/show/181865548). [ci no changelog needed]
This commit is contained in:
parent
66693ad642
commit
656d6e7660
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -2042,6 +2042,7 @@ name = "enso-prelude"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_approx_eq",
|
||||
"backtrace",
|
||||
"boolinator",
|
||||
"cfg-if 1.0.0",
|
||||
@ -2186,7 +2187,6 @@ dependencies = [
|
||||
name = "enso-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assert_approx_eq",
|
||||
"nalgebra 0.26.2",
|
||||
"num-traits",
|
||||
"paste 1.0.7",
|
||||
@ -2252,7 +2252,6 @@ name = "ensogl-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"assert_approx_eq",
|
||||
"bit_field",
|
||||
"code-builder",
|
||||
"console_error_panic_hook",
|
||||
|
@ -1036,8 +1036,8 @@ pub struct SuggestionDatabaseUpdatesEvent {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct LibraryComponent {
|
||||
name: String,
|
||||
shortcut: Option<String>,
|
||||
pub name: String,
|
||||
pub shortcut: Option<String>,
|
||||
}
|
||||
|
||||
/// The component group provided by a library.
|
||||
@ -1045,16 +1045,16 @@ pub struct LibraryComponent {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct LibraryComponentGroup {
|
||||
/// The fully qualified module name. A string consisting of a namespace and a library name
|
||||
/// The fully qualified library name. A string consisting of a namespace and a library name
|
||||
/// separated by the dot <namespace>.<library name>, i.e. `Standard.Base`
|
||||
library: String,
|
||||
pub library: String,
|
||||
/// The group name without the library name prefix. E.g. given the `Standard.Base.Group 1`
|
||||
/// group reference, the `group` field contains `Group 1`.
|
||||
group: String,
|
||||
color: Option<String>,
|
||||
icon: Option<String>,
|
||||
/// group reference, the `name` field contains `Group 1`.
|
||||
pub name: String,
|
||||
pub color: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
/// The list of components provided by this component group.
|
||||
exports: Vec<LibraryComponent>,
|
||||
pub exports: Vec<LibraryComponent>,
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::model::module::QualifiedName as ModuleQualifiedName;
|
||||
use crate::model::suggestion_database::entry as suggestion;
|
||||
use crate::notification::Publisher;
|
||||
|
||||
use engine_protocol::language_server;
|
||||
@ -11,6 +12,7 @@ use engine_protocol::language_server::ExpressionUpdatePayload;
|
||||
use engine_protocol::language_server::MethodPointer;
|
||||
use engine_protocol::language_server::SuggestionId;
|
||||
use engine_protocol::language_server::VisualisationConfiguration;
|
||||
use ensogl::data::color;
|
||||
use flo_stream::Subscriber;
|
||||
use mockall::automock;
|
||||
use serde::Deserialize;
|
||||
@ -272,6 +274,45 @@ pub struct AttachedVisualization {
|
||||
|
||||
|
||||
|
||||
// ======================
|
||||
// === ComponentGroup ===
|
||||
// ======================
|
||||
|
||||
/// A named group of components which is defined in a library imported into an execution context.
|
||||
///
|
||||
/// Components are language elements displayed by the Component Browser. The Component Browser
|
||||
/// displays them in groups defined in libraries imported into an execution context.
|
||||
/// To learn more about component groups, see the [Component Browser Design
|
||||
/// Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md).
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ComponentGroup {
|
||||
pub name: ImString,
|
||||
/// An optional color to use when displaying the component group.
|
||||
pub color: Option<color::Rgb>,
|
||||
pub components: Vec<suggestion::QualifiedName>,
|
||||
}
|
||||
|
||||
impl ComponentGroup {
|
||||
/// Construct from a [`language_server::LibraryComponentGroup`].
|
||||
pub fn from_language_server_protocol_struct(
|
||||
group: language_server::LibraryComponentGroup,
|
||||
) -> Self {
|
||||
let name = group.name.into();
|
||||
let color = group.color.as_ref().and_then(|c| color::Rgb::from_css_hex(c));
|
||||
let components = group.exports.into_iter().map(|e| e.name.into()).collect();
|
||||
ComponentGroup { name, color, components }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<language_server::LibraryComponentGroup> for ComponentGroup {
|
||||
fn from(group: language_server::LibraryComponentGroup) -> Self {
|
||||
Self::from_language_server_protocol_struct(group)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
@ -3,6 +3,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::model::execution_context::AttachedVisualization;
|
||||
use crate::model::execution_context::ComponentGroup;
|
||||
use crate::model::execution_context::ComputedValueInfoRegistry;
|
||||
use crate::model::execution_context::LocalCall;
|
||||
use crate::model::execution_context::Visualization;
|
||||
@ -59,6 +60,8 @@ pub struct ExecutionContext {
|
||||
pub computed_value_info_registry: Rc<ComputedValueInfoRegistry>,
|
||||
/// Execution context is considered ready once it completes it first execution after creation.
|
||||
pub is_ready: crate::sync::Synchronized<bool>,
|
||||
/// Component groups defined in libraries imported into the execution context.
|
||||
pub component_groups: RefCell<Vec<ComponentGroup>>,
|
||||
}
|
||||
|
||||
impl ExecutionContext {
|
||||
@ -69,7 +72,16 @@ impl ExecutionContext {
|
||||
let visualizations = default();
|
||||
let computed_value_info_registry = default();
|
||||
let is_ready = default();
|
||||
Self { logger, entry_point, stack, visualizations, computed_value_info_registry, is_ready }
|
||||
let component_groups = default();
|
||||
Self {
|
||||
logger,
|
||||
entry_point,
|
||||
stack,
|
||||
visualizations,
|
||||
computed_value_info_registry,
|
||||
is_ready,
|
||||
component_groups,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `VisualisationConfiguration` for the visualization with given id. It may be used
|
||||
@ -247,15 +259,17 @@ pub mod test {
|
||||
|
||||
use double_representation::definition::DefinitionName;
|
||||
use double_representation::project;
|
||||
use engine_protocol::language_server;
|
||||
|
||||
#[derive(Clone, Derivative)]
|
||||
#[derivative(Debug)]
|
||||
pub struct MockData {
|
||||
pub module_path: model::module::Path,
|
||||
pub context_id: model::execution_context::Id,
|
||||
pub root_definition: DefinitionName,
|
||||
pub namespace: String,
|
||||
pub project_name: String,
|
||||
pub module_path: model::module::Path,
|
||||
pub context_id: model::execution_context::Id,
|
||||
pub root_definition: DefinitionName,
|
||||
pub namespace: String,
|
||||
pub project_name: String,
|
||||
pub component_groups: Vec<language_server::LibraryComponentGroup>,
|
||||
}
|
||||
|
||||
impl Default for MockData {
|
||||
@ -267,11 +281,12 @@ pub mod test {
|
||||
impl MockData {
|
||||
pub fn new() -> MockData {
|
||||
MockData {
|
||||
context_id: model::execution_context::Id::new_v4(),
|
||||
module_path: crate::test::mock::data::module_path(),
|
||||
root_definition: crate::test::mock::data::definition_name(),
|
||||
namespace: crate::test::mock::data::NAMESPACE_NAME.to_owned(),
|
||||
project_name: crate::test::mock::data::PROJECT_NAME.to_owned(),
|
||||
context_id: model::execution_context::Id::new_v4(),
|
||||
module_path: crate::test::mock::data::module_path(),
|
||||
root_definition: crate::test::mock::data::definition_name(),
|
||||
namespace: crate::test::mock::data::NAMESPACE_NAME.to_owned(),
|
||||
project_name: crate::test::mock::data::PROJECT_NAME.to_owned(),
|
||||
component_groups: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,6 +77,16 @@ impl ExecutionContext {
|
||||
let this = Self { id, model, language_server, logger };
|
||||
this.push_root_frame().await?;
|
||||
info!(this.logger, "Pushed root frame.");
|
||||
match this.load_component_groups().await {
|
||||
Ok(_) => info!(this.logger, "Loaded component groups."),
|
||||
Err(err) => {
|
||||
let msg = iformat!(
|
||||
"Failed to load component groups. No groups will appear in the Favorites \
|
||||
section of the Component Browser. Error: {err}"
|
||||
);
|
||||
error!(this.logger, "{msg}");
|
||||
}
|
||||
}
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
@ -96,6 +106,14 @@ impl ExecutionContext {
|
||||
result.map(|res| res.map_err(|err| err.into()))
|
||||
}
|
||||
|
||||
/// Load the component groups defined in libraries imported into the execution context.
|
||||
async fn load_component_groups(&self) -> FallibleResult {
|
||||
let ls_response = self.language_server.get_component_groups(&self.id).await?;
|
||||
*self.model.component_groups.borrow_mut() =
|
||||
ls_response.component_groups.into_iter().map(|group| group.into()).collect();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Detach visualization from current execution context.
|
||||
///
|
||||
/// Necessary because the Language Server requires passing both visualization ID and expression
|
||||
@ -280,10 +298,11 @@ pub mod test {
|
||||
|
||||
use crate::executor::test_utils::TestWithLocalPoolExecutor;
|
||||
use crate::model::execution_context::plain::test::MockData;
|
||||
use crate::model::execution_context::ComponentGroup;
|
||||
use crate::model::module::QualifiedName;
|
||||
use crate::model::traits::*;
|
||||
|
||||
use engine_protocol::language_server::response::CreateExecutionContext;
|
||||
use engine_protocol::language_server::response;
|
||||
use engine_protocol::language_server::CapabilityRegistration;
|
||||
use engine_protocol::language_server::ExpressionUpdates;
|
||||
use json_rpc::expect_call;
|
||||
@ -303,9 +322,15 @@ pub mod test {
|
||||
fn new_customized(
|
||||
ls_setup: impl FnOnce(&mut language_server::MockClient, &MockData),
|
||||
) -> Fixture {
|
||||
let data = MockData::new();
|
||||
Self::new_customized_with_data(MockData::new(), ls_setup)
|
||||
}
|
||||
|
||||
fn new_customized_with_data(
|
||||
data: MockData,
|
||||
ls_setup: impl FnOnce(&mut language_server::MockClient, &MockData),
|
||||
) -> Fixture {
|
||||
let mut ls_client = language_server::MockClient::default();
|
||||
Self::mock_create_push_destroy_calls(&data, &mut ls_client);
|
||||
Self::mock_default_calls(&data, &mut ls_client);
|
||||
ls_setup(&mut ls_client, &data);
|
||||
ls_client.require_all_calls();
|
||||
let connection = language_server::Connection::new_mock_rc(ls_client);
|
||||
@ -318,13 +343,13 @@ pub mod test {
|
||||
}
|
||||
|
||||
/// What is expected server's response to a successful creation of this context.
|
||||
fn expected_creation_response(data: &MockData) -> CreateExecutionContext {
|
||||
fn expected_creation_response(data: &MockData) -> response::CreateExecutionContext {
|
||||
let context_id = data.context_id;
|
||||
let can_modify =
|
||||
CapabilityRegistration::create_can_modify_execution_context(context_id);
|
||||
let receives_updates =
|
||||
CapabilityRegistration::create_receives_execution_context_updates(context_id);
|
||||
CreateExecutionContext { context_id, can_modify, receives_updates }
|
||||
response::CreateExecutionContext { context_id, can_modify, receives_updates }
|
||||
}
|
||||
|
||||
/// Sets up mock client expectations for context creation and destruction.
|
||||
@ -335,12 +360,9 @@ pub mod test {
|
||||
expect_call!(ls.destroy_execution_context(id) => Ok(()));
|
||||
}
|
||||
|
||||
/// Sets up mock client expectations for context creation, initial frame push
|
||||
/// and destruction.
|
||||
pub fn mock_create_push_destroy_calls(
|
||||
data: &MockData,
|
||||
ls: &mut language_server::MockClient,
|
||||
) {
|
||||
/// Sets up mock client expectations for all the calls issued by default by the
|
||||
/// [`ExecutionContext::create`] and [`ExecutionContext::drop`] methods.
|
||||
pub fn mock_default_calls(data: &MockData, ls: &mut language_server::MockClient) {
|
||||
Self::mock_create_destroy_calls(data, ls);
|
||||
let id = data.context_id;
|
||||
let root_frame = language_server::ExplicitCall {
|
||||
@ -350,6 +372,10 @@ pub mod test {
|
||||
};
|
||||
let stack_item = language_server::StackItem::ExplicitCall(root_frame);
|
||||
expect_call!(ls.push_to_execution_context(id,stack_item) => Ok(()));
|
||||
let component_groups = language_server::response::GetComponentGroups {
|
||||
component_groups: data.component_groups.clone(),
|
||||
};
|
||||
expect_call!(ls.get_component_groups(id) => Ok(component_groups));
|
||||
}
|
||||
|
||||
/// Generates a mock update for a random expression id.
|
||||
@ -513,4 +539,68 @@ pub mod test {
|
||||
context.modify_visualization(vis_id, expression, module).await.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
/// Check that the [`ExecutionContext::load_component_groups`] method correctly parses
|
||||
/// a mocked Language Server response and loads the result into a field of the
|
||||
/// [`ExecutionContext`].
|
||||
#[test]
|
||||
fn loading_component_groups() {
|
||||
// Prepare sample component groups to be returned by a mock Language Server client.
|
||||
fn library_component(name: &str) -> language_server::LibraryComponent {
|
||||
language_server::LibraryComponent { name: name.to_string(), shortcut: None }
|
||||
}
|
||||
let sample_ls_component_groups = vec![
|
||||
// A sample component group in local namespace, with non-empty color, and with exports
|
||||
// from the local namespace as well as from the standard library.
|
||||
language_server::LibraryComponentGroup {
|
||||
library: "local.Unnamed_10".to_string(),
|
||||
name: "Test Group 1".to_string(),
|
||||
color: Some("#C047AB".to_string()),
|
||||
icon: None,
|
||||
exports: vec![
|
||||
library_component("Standard.Base.System.File.new"),
|
||||
library_component("local.Unnamed_10.Main.main"),
|
||||
],
|
||||
},
|
||||
// A sample component group from the standard library, without a predefined color.
|
||||
language_server::LibraryComponentGroup {
|
||||
library: "Standard.Base".to_string(),
|
||||
name: "Input".to_string(),
|
||||
color: None,
|
||||
icon: None,
|
||||
exports: vec![library_component("Standard.Base.System.File.new")],
|
||||
},
|
||||
];
|
||||
|
||||
// Create a test fixture based on the sample data.
|
||||
let mut mock_data = MockData::new();
|
||||
mock_data.component_groups = sample_ls_component_groups;
|
||||
let fixture = Fixture::new_customized_with_data(mock_data, |_, _| {});
|
||||
let Fixture { mut test, context, .. } = fixture;
|
||||
|
||||
// Run a test and verify that the sample component groups were parsed correctly and have
|
||||
// expected contents.
|
||||
test.run_task(async move {
|
||||
let groups = context.model.component_groups.borrow();
|
||||
assert_eq!(groups.len(), 2);
|
||||
|
||||
// Verify that the first component group was parsed and has expected contents.
|
||||
let first_group = &groups[0];
|
||||
assert_eq!(first_group.name, "Test Group 1".to_string());
|
||||
let color = first_group.color.unwrap();
|
||||
assert_eq!((color.red * 255.0) as u8, 0xC0);
|
||||
assert_eq!((color.green * 255.0) as u8, 0x47);
|
||||
assert_eq!((color.blue * 255.0) as u8, 0xAB);
|
||||
let expected_components =
|
||||
vec!["Standard.Base.System.File.new".into(), "local.Unnamed_10.Main.main".into()];
|
||||
assert_eq!(first_group.components, expected_components);
|
||||
|
||||
// Verify that the second component group was parsed and has expected contents.
|
||||
assert_eq!(groups[1], ComponentGroup {
|
||||
name: "Input".into(),
|
||||
color: None,
|
||||
components: vec!["Standard.Base.System.File.new".into(),],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -810,7 +810,7 @@ mod test {
|
||||
let context_data = execution_context::plain::test::MockData::new();
|
||||
let Fixture { mut test, project, json_events_sender, .. } = Fixture::new(
|
||||
|mock_json_client| {
|
||||
ExecutionFixture::mock_create_push_destroy_calls(&context_data, mock_json_client);
|
||||
ExecutionFixture::mock_default_calls(&context_data, mock_json_client);
|
||||
mock_json_client.require_all_calls();
|
||||
},
|
||||
|_| {},
|
||||
|
@ -70,6 +70,18 @@ pub struct QualifiedName {
|
||||
pub segments: Vec<QualifiedNameSegment>,
|
||||
}
|
||||
|
||||
impl From<&str> for QualifiedName {
|
||||
fn from(name: &str) -> Self {
|
||||
name.split(ast::opr::predefined::ACCESS).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for QualifiedName {
|
||||
fn from(name: String) -> Self {
|
||||
name.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<QualifiedName> for String {
|
||||
fn from(name: QualifiedName) -> Self {
|
||||
String::from(&name)
|
||||
|
@ -1530,7 +1530,7 @@ The component group provided by a library.
|
||||
```typescript
|
||||
interface LibraryComponentGroup {
|
||||
/**
|
||||
* Thf fully qualified module name. A string consisting of a namespace and
|
||||
* The fully qualified library name. A string consisting of a namespace and
|
||||
* a library name separated by the dot <namespace>.<library name>,
|
||||
* i.e. `Standard.Base`.
|
||||
*/
|
||||
@ -1538,9 +1538,9 @@ interface LibraryComponentGroup {
|
||||
|
||||
/** The group name without the library name prefix.
|
||||
* E.g. given the `Standard.Base.Group 1` group reference,
|
||||
* the `group` field contains `Group 1`.
|
||||
* the `name` field contains `Group 1`.
|
||||
*/
|
||||
group: string;
|
||||
name: string;
|
||||
|
||||
color?: string;
|
||||
|
||||
|
@ -62,7 +62,7 @@ final class ComponentGroupsResolver {
|
||||
.groupByKeepFirst(newLibraryComponentGroups) { libraryComponentGroup =>
|
||||
GroupReference(
|
||||
libraryComponentGroup.library,
|
||||
libraryComponentGroup.group
|
||||
libraryComponentGroup.name
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -76,14 +76,14 @@ object LibraryComponentGroups {
|
||||
* the JSONRPC API.
|
||||
*
|
||||
* @param library the library name
|
||||
* @param group the group name
|
||||
* @param name the group name
|
||||
* @param color the component group color
|
||||
* @param icon the component group icon
|
||||
* @param exports the list of components provided by this component group
|
||||
*/
|
||||
case class LibraryComponentGroup(
|
||||
library: LibraryName,
|
||||
group: GroupName,
|
||||
name: GroupName,
|
||||
color: Option[String],
|
||||
icon: Option[String],
|
||||
exports: Seq[LibraryComponent]
|
||||
@ -103,7 +103,7 @@ object LibraryComponentGroup {
|
||||
): LibraryComponentGroup =
|
||||
LibraryComponentGroup(
|
||||
library = libraryName,
|
||||
group = componentGroup.group,
|
||||
name = componentGroup.group,
|
||||
color = componentGroup.color,
|
||||
icon = componentGroup.icon,
|
||||
exports = componentGroup.exports.map(LibraryComponent.fromComponent)
|
||||
@ -120,7 +120,7 @@ object LibraryComponentGroup {
|
||||
): LibraryComponentGroup =
|
||||
LibraryComponentGroup(
|
||||
library = extendedComponentGroup.group.libraryName,
|
||||
group = extendedComponentGroup.group.groupName,
|
||||
name = extendedComponentGroup.group.groupName,
|
||||
color = None,
|
||||
icon = None,
|
||||
exports =
|
||||
@ -130,7 +130,7 @@ object LibraryComponentGroup {
|
||||
/** Fields for use when serializing the [[LibraryComponentGroup]]. */
|
||||
private object Fields {
|
||||
val Library = "library"
|
||||
val Group = "group"
|
||||
val Name = "name"
|
||||
val Color = "color"
|
||||
val Icon = "icon"
|
||||
val Exports = "exports"
|
||||
@ -145,7 +145,7 @@ object LibraryComponentGroup {
|
||||
)
|
||||
Json.obj(
|
||||
(Fields.Library -> componentGroup.library.asJson) +:
|
||||
(Fields.Group -> componentGroup.group.asJson) +:
|
||||
(Fields.Name -> componentGroup.name.asJson) +:
|
||||
(color.toSeq ++ icon.toSeq ++ exports.toSeq): _*
|
||||
)
|
||||
}
|
||||
@ -154,11 +154,11 @@ object LibraryComponentGroup {
|
||||
implicit val decoder: Decoder[LibraryComponentGroup] = { json =>
|
||||
for {
|
||||
library <- json.get[LibraryName](Fields.Library)
|
||||
group <- json.get[GroupName](Fields.Group)
|
||||
name <- json.get[GroupName](Fields.Name)
|
||||
color <- json.get[Option[String]](Fields.Color)
|
||||
icon <- json.get[Option[String]](Fields.Icon)
|
||||
exports <- json.getOrElse[List[LibraryComponent]](Fields.Exports)(List())
|
||||
} yield LibraryComponentGroup(library, group, color, icon, exports)
|
||||
} yield LibraryComponentGroup(library, name, color, icon, exports)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -301,14 +301,14 @@ object ComponentGroupsResolverSpec {
|
||||
|
||||
/** Create a new library component group. */
|
||||
def libraryComponentGroup(
|
||||
namespace: String,
|
||||
name: String,
|
||||
group: String,
|
||||
libraryNamespace: String,
|
||||
libraryName: String,
|
||||
groupName: String,
|
||||
exports: String*
|
||||
): LibraryComponentGroup =
|
||||
LibraryComponentGroup(
|
||||
library = LibraryName(namespace, name),
|
||||
group = GroupName(group),
|
||||
library = LibraryName(libraryNamespace, libraryName),
|
||||
name = GroupName(groupName),
|
||||
color = None,
|
||||
icon = None,
|
||||
exports = exports.map(LibraryComponent(_, None))
|
||||
|
@ -921,7 +921,7 @@ class ContextRegistryTest extends BaseServerTest {
|
||||
"componentGroups": [
|
||||
{
|
||||
"library" : "Standard.Base",
|
||||
"group" : "Input",
|
||||
"name" : "Input",
|
||||
"exports" : [
|
||||
{
|
||||
"name" : "Standard.Base.File.new"
|
||||
|
@ -87,5 +87,4 @@ features = [
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
assert_approx_eq = { version = "1.1.0" }
|
||||
wasm-bindgen-test = { version = "0.3.8" }
|
||||
|
@ -389,6 +389,51 @@ impl Rgb {
|
||||
Self::new(r.into() / 255.0, g.into() / 255.0, b.into() / 255.0)
|
||||
}
|
||||
|
||||
/// Return a color if the argument is a string matching one of the formats: `#RGB`, `#RRGGBB`,
|
||||
/// `RGB`, or `RRGGBB`, where `R`, `G`, `B` represent lower- or upper-case hexadecimal digits.
|
||||
/// The `RR`, `GG`, `BB` color components are mapped from `[00 - ff]` value range into `[0.0 -
|
||||
/// 1.0]` (a three-digit string matching a `#RGB` or `RGB` format is equivalent to a six-digit
|
||||
/// string matching a `#RRGGBB` format, constructed by duplicating the digits).
|
||||
///
|
||||
/// The format is based on the hexadecimal color notation used in CSS (see:
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color), with the following changes:
|
||||
/// - the `#` character is optional,
|
||||
/// - formats containing an alpha color component are not supported.
|
||||
/// ```
|
||||
/// # use ensogl_core::data::color::Rgb;
|
||||
/// fn color_to_u8_tuple(c: Rgb) -> (u8, u8, u8) {
|
||||
/// ((c.red * 255.0) as u8, (c.green * 255.0) as u8, (c.blue * 255.0) as u8)
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(Rgb::from_css_hex("#C047AB").map(color_to_u8_tuple), Some((0xC0, 0x47, 0xAB)));
|
||||
/// assert_eq!(Rgb::from_css_hex("#fff").map(color_to_u8_tuple), Some((0xff, 0xff, 0xff)));
|
||||
/// assert_eq!(Rgb::from_css_hex("fff").map(color_to_u8_tuple), Some((0xff, 0xff, 0xff)));
|
||||
/// assert_eq!(Rgb::from_css_hex("C047AB").map(color_to_u8_tuple), Some((0xC0, 0x47, 0xAB)));
|
||||
/// assert!(Rgb::from_css_hex("red").is_none());
|
||||
/// assert!(Rgb::from_css_hex("yellow").is_none());
|
||||
/// assert!(Rgb::from_css_hex("#red").is_none());
|
||||
/// assert!(Rgb::from_css_hex("#yellow").is_none());
|
||||
/// assert!(Rgb::from_css_hex("#").is_none());
|
||||
/// assert!(Rgb::from_css_hex("").is_none());
|
||||
/// ```
|
||||
pub fn from_css_hex(css_hex: &str) -> Option<Self> {
|
||||
let hex_bytes = css_hex.strip_prefix('#').unwrap_or(css_hex).as_bytes();
|
||||
let hex_color_components = match hex_bytes.len() {
|
||||
3 => Some(Vector3([hex_bytes[0]; 2], [hex_bytes[1]; 2], [hex_bytes[2]; 2])),
|
||||
6 => {
|
||||
let (chunks, _) = hex_bytes.as_chunks::<2>();
|
||||
Some(Vector3(chunks[0], chunks[1], chunks[2]))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
hex_color_components.and_then(|components| {
|
||||
let red = byte_from_hex(components.x)?;
|
||||
let green = byte_from_hex(components.y)?;
|
||||
let blue = byte_from_hex(components.z)?;
|
||||
Some(Rgb::from_base_255(red, green, blue))
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the color to `LinearRgb` representation.
|
||||
pub fn into_linear(self) -> LinearRgb {
|
||||
self.into()
|
||||
@ -403,6 +448,21 @@ impl Rgb {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Rgb Helpers ===
|
||||
|
||||
/// Decode an 8-bit number from its big-endian hexadecimal encoding in ASCII. Return `None` if any
|
||||
/// of the bytes stored in the argument array is not an upper- or lower-case hexadecimal digit in
|
||||
/// ASCII.
|
||||
fn byte_from_hex(s: [u8; 2]) -> Option<u8> {
|
||||
let first_digit = (s[0] as char).to_digit(16)? as u8;
|
||||
let second_digit = (s[1] as char).to_digit(16)? as u8;
|
||||
Some(first_digit << 4 | second_digit)
|
||||
}
|
||||
|
||||
|
||||
// === Rgba ===
|
||||
|
||||
impl Rgba {
|
||||
/// Constructor.
|
||||
pub fn black() -> Self {
|
||||
|
@ -821,8 +821,6 @@ mod tests {
|
||||
use enso_web::TimeProvider;
|
||||
use std::ops::AddAssign;
|
||||
|
||||
use assert_approx_eq::assert_approx_eq;
|
||||
|
||||
|
||||
// === MockTimeProvider ===
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(trace_macros)]
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(slice_as_chunks)]
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
|
@ -201,8 +201,11 @@ macro_rules! make_rpc_methods {
|
||||
impl Drop for Client {
|
||||
fn drop(&mut self) {
|
||||
if self.require_all_calls.get() && !std::thread::panicking() {
|
||||
$(assert!(self.expect.$method.borrow().is_empty(),
|
||||
"Didn't make expected call");)* //TODO[ao] print method name.
|
||||
$(
|
||||
let method = stringify!($method);
|
||||
let msg = iformat!("An expected call to {method} was not made.");
|
||||
assert!(self.expect.$method.borrow().is_empty(), "{}", msg);
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
enso-shapely = { version = "^0.2.0", path = "../shapely" }
|
||||
anyhow = "1.0.37"
|
||||
assert_approx_eq = { version = "1.1.0" }
|
||||
backtrace = "0.3.53"
|
||||
boolinator = "2.4.0"
|
||||
cfg-if = "1.0.0"
|
||||
|
@ -68,6 +68,7 @@ pub use tp::*;
|
||||
pub use vec::*;
|
||||
pub use wrapper::*;
|
||||
|
||||
pub use assert_approx_eq::assert_approx_eq;
|
||||
pub use boolinator::Boolinator;
|
||||
pub use derivative::Derivative;
|
||||
pub use derive_more::*;
|
||||
|
@ -10,6 +10,3 @@ edition = "2021"
|
||||
nalgebra = { version = "0.26.1" }
|
||||
num-traits = { version = "0.2" }
|
||||
paste = "1.0.7"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_approx_eq = { version = "1.1.0" }
|
||||
|
Loading…
Reference in New Issue
Block a user