Skip to content

Conversation

tomcur
Copy link
Member

@tomcur tomcur commented Nov 13, 2024

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

  • a color declared as a named CSS/X11 color, should serialize to that (normalized) color name; and
  • a color declared as one of the named color space functions – i.e., not the color() function – should serialize to that function. (Exceptions being hsl() and hwb(), those serialize to rgb().)

This implementation uses the padding currently available on DynamicColor to pack in the color name the DynamicColor was created from, or whether a named color space function was used; an implementation of the ideas in #26 (comment).

DynamicColor now contains Flags, and the existing Missing color components are moved into Flags. In this implementation, only parse_color sets the color name or color space function name.

@tomcur tomcur force-pushed the packed-names branch 2 times, most recently from ac22029 to 9a13385 Compare November 13, 2024 16:23
Copy link
Member

@DJMcNab DJMcNab left a 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.

// 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);
Copy link
Member

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)

Copy link
Collaborator

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.

Copy link
Member Author

@tomcur tomcur Nov 14, 2024

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.

/// 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> {
Copy link
Member

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?

Copy link
Member Author

@tomcur tomcur Nov 16, 2024

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",
    ),
}

Copy link
Member

@DJMcNab DJMcNab left a 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?

@tomcur
Copy link
Member Author

tomcur commented Nov 16, 2024

What's needed to move this out of draft?

I think when #39 (comment) is resolved, we can move this out of draft.

@tomcur tomcur marked this pull request as ready for review November 18, 2024 10:37
@tomcur tomcur changed the title Pack color name into DynamicColor flags Serialize color names by packing them into DynamicColor Nov 18, 2024
Comment on lines 164 to 190
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)"),
] {
Copy link
Member Author

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.

@waywardmonkeys waywardmonkeys added this to the 0.2.0 milestone Nov 22, 2024
@raphlinus
Copy link
Contributor

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.

@tomcur
Copy link
Member Author

tomcur commented Dec 2, 2024

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).

@waywardmonkeys
Copy link
Collaborator

@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.

@tomcur
Copy link
Member Author

tomcur commented Dec 10, 2024

That is true, but they'd still need custom parsing, unfortunately. The CSS spec says the color parsed from the string red serializes to red, rgb(1, 0, 0) serializes to rgb(1, 0, 0) and color(srgb 1 0 0) serializes to color(srgb 1 0 0). To be able to do that, some information about the source has to be retained, either in DynamicColor, or as part of the return value from parse_color. If this library provides no support for that, it has to be through a custom parser outside of the library.

@waywardmonkeys
Copy link
Collaborator

Good points. I lean towards this landing for 0.2. @raphlinus ?

Copy link
Contributor

@raphlinus raphlinus left a 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.

@nicoburns
Copy link
Contributor

I also like the idea of the code being minimal, and there are applications that will not need this functionality.

The main argument to include it would be that it's not straightforward to implement on top of the library.

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]>
}

pub(crate) fn lookup_palette(s: &str) -> Option<[u8; 4]> {
pub(crate) fn lookup_palette(s: &str) -> Option<usize> {
Copy link
Collaborator

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rename: e227926, 013c90b.

Moving it around is a bit more difficult as the file is auto-generated, but I'm unsure what you mean by duplication of the function.

Comment on lines +57 to +60
#[warn(
clippy::missing_const_for_fn,
reason = "can be made const with MSRV 1.83"
)]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added warning lints in 70c1983 as suggested in #91, but interestingly Clippy 0.1.83 doesn't warn. I did bump workspace.package.rust-version in Cargo.toml.

Copy link
Member

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.

Copy link
Member Author

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

@tomcur
Copy link
Member Author

tomcur commented Dec 12, 2024

Rebasing is done, but I also made some larger changes to color/src/serialize.rs especially. It's along the same lines, but fixes some edge cases that I've now also added as tests. I think another look from someone before merging is a good idea.

Copy link
Member

@DJMcNab DJMcNab left a 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) {
Copy link
Member

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)

Copy link
Member Author

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),
Copy link
Member

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...

Copy link
Member Author

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());
Copy link
Member

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...

Copy link
Member Author

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...

@tomcur tomcur added this pull request to the merge queue Dec 12, 2024
Merged via the queue into linebender:main with commit 6c4419c Dec 12, 2024
15 checks passed
@tomcur tomcur deleted the packed-names branch December 12, 2024 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants