Serialize color names by packing them into DynamicColor#39
Serialize color names by packing them into DynamicColor#39tomcur merged 19 commits intolinebender:mainfrom
DynamicColor#39Conversation
ac22029 to
9a13385
Compare
DJMcNab
left a comment
There was a problem hiding this comment.
Looks good! Some more passing thoughts.
color/src/missing.rs
Outdated
| // indicates whether the color was generated from a named color or named color space function. The | ||
| // remaining bits indicate the named color. | ||
| #[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] | ||
| pub struct Flags(u16); |
There was a problem hiding this comment.
I don't really like packing the missing/named segments together when we still have 8 bits of unused padding. I don't think it's especially obvious that users will be constructing DynamicColor from its raw components.
(This comment is more to start discussion, not to request an immediately actioned change)
There was a problem hiding this comment.
Raph had mentioned (somewhere) that he would rename Missing to Flags or something if he went down this path, I think.
There was a problem hiding this comment.
DynamicColor can fit 24 additional bits (of which Missing currently takes 8). That can be done with an 8-bit type and a 16-bit type. If ergonomics of constructing DynamicColor doesn't matter too much, I think I would also prefer using two fields. Alternatively, Flags could become
struct Flags {
missing: u8,
name: u8,
}(leaving 8 bits unused), where, for example, name == 0 indicates the value is not from a named color or named color space, name == 255 indicates it is from a named color space function, and other values indicate it is from a named color and are the 1-based index into crate::x11_colors::NAMES.
color/src/missing.rs
Outdated
| /// If the color was constructed from a named color, returns that name. | ||
| /// | ||
| /// See also [`parse_color`][crate::parse_color]. | ||
| pub fn color_name(self) -> Option<&'static str> { |
There was a problem hiding this comment.
Can we use this in Debug somewhere?
There was a problem hiding this comment.
As Flags and Missing are opaque, we could Debug format to, for example:
Flags {
missing: Missing(
0b00000000,
),
name: 33,
named: true,
color_name: Some(
"red",
),
}
DJMcNab
left a comment
There was a problem hiding this comment.
What's needed to move this out of draft?
I think when #39 (comment) is resolved, we can move this out of draft. |
DynamicColor flagsDynamicColor
| for (specified, expected) in [ | ||
| ("rgb(1,1,1)", "rgb(1, 1, 1)"), | ||
| // TODO: output rounding? Otherwise the tests should check for approximate equality | ||
| // (and not string equality) for these conversion cases | ||
| ( | ||
| "hwb(740deg 20% 30% / 50%)", | ||
| "rgba(178.5, 93.50008, 50.999996, 0.5)", | ||
| ), | ||
| // the next two currently fail, but should succeed (ASCII uppercase codepoints should | ||
| // be lowercased) | ||
| // ("ReD", "red"), | ||
| // ("RgB(1,1,1)", "rgb(1, 1, 1)"), | ||
| // currently fails, but should succeed (values should be clamped at parse-time) | ||
| // ("rgb(1.1,1,1)", "rgb(1, 1, 1)"), | ||
| ("color(srgb 1.0 1.0 1.0)", "color(srgb 1 1 1)"), | ||
| ] { |
There was a problem hiding this comment.
I believe this is the correct serialization as specified by CSS Color 4. To leave case normalization out of the crate, callers wanting case-insensitivity could be instructed to str::make_ascii_lowercase. Otherwise the parser requires some work to get performant insensitive matching.
|
I'm having trouble making a go/no-go decision on this. I like the idea of better CSS compatibility, and the implementation seems fine (though I didn't dig deep), but I also like the idea of the code being minimal, and there are applications that will not need this functionality. I know we talked about this in office hours but don't feel consensus emerged. Does anyone have a strong feeling either way? My gut feeling is that it's easier to add later if there's a strong need than remove, but if it is going in anyway, now is a good time to do it. |
|
I don't have strong feelings about this. The main argument to include it would be that it's not straightforward to implement on top of the library. Users would have to implement their own parsing and create a wrapping type. It certainly grows the lines of code though, but at least has little runtime impact (and no memory impact). |
|
@tomcur They wouldn't necessarily have to do a wrapping type ...they could just do a very quick linear scan of the colors for a matching one. Sure, it is moderately gross, but it is something they'd opt into. |
|
That is true, but they'd still need custom parsing, unfortunately. The CSS spec says the color parsed from the string |
|
Good points. I lean towards this landing for 0.2. @raphlinus ? |
raphlinus
left a comment
There was a problem hiding this comment.
I am ok with landing this, it seems like a nice convenience for users, and I'm not too worried about the cost.
This sounds very much like it ought to exist behind a feature flag to me! (but I think the short term it doesn't matter too much - we can always and then feature flag it in the future version). |
A rough implementation of the ideas in linebender#26 (comment). The ergonomics require some tweaking, and perhaps the performance too, though it should be easy to write this in a form where the compiler can easily optimize the bitwise operations. I haven't verified what the compiler does with the current form. An alternative is to keep `Missing` unchanged, and add something similar to `Flags` as a new field on `DynamicColor`. But adding a new field makes constructing `DynamicColor` a bit less ergonomic. Squashed: hsl() and hwb() serialize to rgb() Naming consistency Check size only during test Test all named colors Mixed case test Separate into two `u8`s consistency Debug impl Clean up Clippy Use Missing directly Reduce api surface Module name Simplify Debug Module header Inline more Docs --------- Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
color/src/x11_colors.rs
Outdated
| } | ||
|
|
||
| pub(crate) fn lookup_palette(s: &str) -> Option<[u8; 4]> { | ||
| pub(crate) fn lookup_palette(s: &str) -> Option<usize> { |
There was a problem hiding this comment.
Could you rename this to lookup_palette_index or similar? Maybe even with css_palette? Or should this not be duplicated and moved to the palette code with a single function definition?
| #[warn( | ||
| clippy::missing_const_for_fn, | ||
| reason = "can be made const with MSRV 1.83" | ||
| )] |
There was a problem hiding this comment.
Hmm. I guess it doesn't know about the mutability change.
There was a problem hiding this comment.
Will be caught in a future release: rust-lang/rust-clippy#13839
|
Rebasing is done, but I also made some larger changes to |
DJMcNab
left a comment
There was a problem hiding this comment.
I've not reasoned about this too carefully, but the code quality is excellent
|
|
||
| /// Set the flags to indicate the color was specified as one of the named colors. `name_ix` is | ||
| /// the index into [`crate::x11_colors::NAMES`]. | ||
| pub(crate) fn set_named_color(&mut self, name_ix: usize) { |
There was a problem hiding this comment.
This will never be called from user code, right? (I ask because otherwise color_name could panic in release mode)
There was a problem hiding this comment.
That's right, only the parser calls this.
| "oklch" => parser.lch(1.0, 0.004, ColorSpaceTag::Oklch), | ||
| "hsl" | "hsla" => parser.hsl(), | ||
| "hwb" => parser.hwb(), | ||
| "rgb" | "rgba" => parser.rgb().map(set_from_named_color_space), |
There was a problem hiding this comment.
Not a huge fan of this pattern, but I'm not sure how to avoid it...
There was a problem hiding this comment.
You mean the map(set_from_named_color_space)? I agree; I remember I tried some alternatives and that this pattern was the least bad.
| #[test] | ||
| fn roundtrip_named_colors() { | ||
| for name in crate::x11_colors::NAMES { | ||
| let result = format!("{}", parse_color(name).unwrap()); |
There was a problem hiding this comment.
Technically all of these tests could share the same allocated String. That probably wouldn't actually be better...
There was a problem hiding this comment.
True, though I think the current conciseness is also nice. But if it were thousands of strings...
To perform correct CSS Color 4 serialization of colors, some details of how a color was declared have to be preserved. Specifically, CSS Color Module Level 4 § 15.2 and the two sections on Lab and Oklab following it specify that
color()function – should serialize to that function. (Exceptions beinghsl()andhwb(), those serialize torgb().)This implementation uses the padding currently available on
DynamicColorto pack in the color name theDynamicColorwas created from, or whether a named color space function was used; an implementation of the ideas in #26 (comment).DynamicColornow containsFlags, and the existingMissingcolor components are moved intoFlags. In this implementation, onlyparse_colorsets the color name or color space function name.