-
Notifications
You must be signed in to change notification settings - Fork 9
Serialize color names by packing them into DynamicColor
#39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
ac22029
to
9a13385
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use this in Debug
somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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",
),
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 <[email protected]>
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[warn( | ||
clippy::missing_const_for_fn, | ||
reason = "can be made const with MSRV 1.83" | ||
)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I guess it doesn't know about the mutability change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will be caught in a future release: rust-lang/rust-clippy#13839
Rebasing is done, but I also made some larger changes to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a huge fan of this pattern, but I'm not sure how to avoid it...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
DynamicColor
to pack in the color name theDynamicColor
was created from, or whether a named color space function was used; an implementation of the ideas in #26 (comment).DynamicColor
now containsFlags
, and the existingMissing
color components are moved intoFlags
. In this implementation, onlyparse_color
sets the color name or color space function name.