Skip to content
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

Optional and opaque RGB colors #2472

Merged
merged 35 commits into from
Feb 10, 2020
Merged

Optional and opaque RGB colors #2472

merged 35 commits into from
Feb 10, 2020

Conversation

uklotzde
Copy link
Contributor

@uklotzde uklotzde commented Jan 29, 2020

QColor is too generic for representing opaque RGB colors with 8-bit per channel and a corresponding 24-bit color code. This new class handles all validation, normalization and conversion by wrapping QColor. It is more restrictive than QColor, but preserves the optional (= invalid) state.

This class could be used for all kinds of color metadata, both track colors (#2470) and cue colors. We might even think about making cue colors optional, because QColor is implicitly optional and we would need to handle it anyway! I should have noticed this before 🙈 But my idea of such a wrapper class was rejected when I mentioned that we need such an abstraction and so I didn't dig deeper into the topic.

The unit tests revealed that the handling is trickier than expected and that directly dealing with QColor would be error prone.

@uklotzde uklotzde changed the title Optional, opaque RGB colors Optional and opaque RGB colors Jan 29, 2020
@daschuer
Copy link
Member

Thank you for this work.

When writing the cue color code I did consider to write it as well, but than decided to just use QRgb. Than I have discovered the bug with QColor QRgb and the opacity.
So it is good to have this.

Did you consider to use RgbColorCode as storage variable. This has some benefit:

  • always valid
  • Instant conversions to/from double value vor CO
  • Instant conversions to/from DB and external interfaces.
  • Single instruction copy (QColor is not implicit shared)

This comes with the cost of constructing QColor when needed. But the construction is not that heavy: https://github.com/mburakov/qt5/blob/93bfa3874c10f6cb5aa376f24363513ba8264117/qtbase/src/gui/painting/qcolor.cpp#L418

I don't have a benchmark yet, but I guess it will over all perform better.

What do you think?

@uklotzde uklotzde changed the title Optional and opaque RGB colors [WiP] Optional and opaque RGB colors Jan 29, 2020
@uklotzde
Copy link
Contributor Author

use RgbColorCode as storage variable

What does that mean, any links? Please elaborate.

}
}

QColor m_color;
Copy link
Member

Choose a reason for hiding this comment

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

I mean this:

RgbColorCode m_color

Copy link
Contributor Author

Choose a reason for hiding this comment

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

use RgbColorCode as storage variable

What does that mean, any links? Please elaborate.

See my inline comment. You may use 0xFFFFFFFF for invalid. That automatically defaults to transparent, which is what we want in most cases.

Nope. Alpha = 0x00 is transparent.

We agreed on using QColor internally and this is the consequent follow-up and reasonable software design for such an approach. RgbColor allows even less variations than QColor.

We could also store RgbColorCode internally with alpha = 0xFF for all valid opaque colors and 0x00000000 as a special value for invalid/transparent/no color. But this custom mapping must not be exposed publicly! The only conversions that are provided are to/from QColor and std::optional<RgbColorCode>. The public API wouldn't change.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that was the Idea to keep the whole thing performant.

What is the public API you reffering to?
We have the CO interface that uses the 24 bit color as double, the ES interface that uses tree values for colors, the file tag that uses also a 24 bit value.
So it looks like QColor is only required to feed the Qt painting interface.

Copy link
Contributor

Choose a reason for hiding this comment

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

invalid/transparent/no color

Let's be clear with language. These are not identical concepts. There are contexts in which transparent is a valid value such as track color; calling it invalid would be a misnomer.

@daschuer
Copy link
Member

use RgbColorCode as storage variable

What does that mean, any links? Please elaborate.

See my inline comment. You may use 0xFFFFFFFF for invalid. That automatically defaults to transparent, which is what we want in most cases.

@uklotzde
Copy link
Contributor Author

I have turned the class upside down by replacing the internal QColor with RgbColorCode.

I didn't need to change a single test! IMHO a strong indicator that the design is robust and that this is the way to go.

@uklotzde uklotzde changed the title [WiP] Optional and opaque RGB colors Optional and opaque RGB colors Jan 29, 2020
@uklotzde
Copy link
Contributor Author

PS: Let's please stop the discussions about negligible performance improvements and instead get the design right and work done.

@uklotzde uklotzde changed the title Optional and opaque RGB colors [WiP] Optional and opaque RGB colors Jan 29, 2020
src/util/color/rgbcolor.h Outdated Show resolved Hide resolved
src/util/color/rgbcolor.h Outdated Show resolved Hide resolved
src/util/color/rgbcolor.h Outdated Show resolved Hide resolved
Copy link
Contributor

@ferranpujolcamins ferranpujolcamins left a comment

Choose a reason for hiding this comment

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

As already expressed, my biggest disagreement is with the fact that RgbColor can hold an invalid color.

Instead, I think it's best to make RgbColor represent only valid colors, then any function converting from QColor to RgbColor can return std::optional<RgbColor>.

class RgbColor {
public:
RgbColor() {
DEBUG_ASSERT(!isValid());
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need the debug assertion? This is already covered by the tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

Furthermore, we could discuss whether we want the "optionality" to be baked in RgbColor. From my point of view, it'd more flexible to have a type that represents a valid color. If you want an optional color, that's what std::optional is for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We must handle it, because QColor requires us to do so. Otherwise this class could not be agnostic and would require use case specific quirks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The mapping to non-optional values has to be done on the next abstraction level, not here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Debug assertions might reveal bugs that we didn't anticipate in the tests. Yes, these are helpful and intended!

Copy link
Contributor

@ferranpujolcamins ferranpujolcamins Jan 30, 2020

Choose a reason for hiding this comment

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

What I mean is:

If I understood correctly, RgbColor is just RgbColorCode with a special value to indicate an invalid color (and some validation to make sure we don't accidentally set an invalid color).

Instead, I propose to use std::optional<RgbColorCode>. Here we don't treat any value of RgbColorCode as "invalid", so we can just ignore the upper bits.

With my suggested approach, we get !=, == and bool() for free from std::optional, while you needed to implement them for RgbColor.

Furthermore, the following methods are no longer needed:

  • optional()
  • optionalCodeToInternalCode (any RgbColorCode is valid, we just ignore the upper bits)

The only code you actually need to write is a bunch of free functions to convert back and forth QColor:

QColor toQColor(RgbColorCode color) {
    return QColor::fromRgb(m_internalCode & kRgbCodeMask);
}

QColor toQColor(std::optional<RgbColorCode> color) {
    if (color) {
        return toQColor(*color);
    } else {
        return QColor();
    }
}

std::optional<RgbColorCode> toRgb(QColor color) {
    if (anyColor.isValid()) {
        return std::make_optional<RgbColorCode>(anyColor.rgb())
    } else {
        return std::optional<RgbColorCode>()
    }
}

Another advantage of this approach is that you can express whether you accept invalid colors or not in a function, by asking for a std::optional<RgbColorCode> or a RgbColorCode. This is not possible with your proposed approach.


All this being said, there's no need for more discussion unless you have some questions about what I've written here.

If you feel you approach is best, I'm fine. Just wanted you to consider this other one :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ferranpujolcamins Hm, I need to think about this again. Your remarks about optionality are valid.

I don't have any stakes in here and just wanted to demonstrate that we need some kind of abstraction. But it quickly escalated in terms of work effort ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, sometimes just happens that your abstraction can just be a type alias, because what you need is already there ;)

src/util/color/rgbcolor.h Show resolved Hide resolved
@uklotzde
Copy link
Contributor Author

If you have different ideas, just do it.

I can only strongly recommend to add appropriate abstraction layers. This tiny class in conjunction with the tests revealed how complex the situation really is. Be warned, there are a many pitfalls that might not be obvious! ...including dirty and time consuming struggles with ancient C++ compilers :((

@uklotzde
Copy link
Contributor Author

@Holzhaus
Proof of concept: Optional track colors

// without an an alpha channel.
// We are using a separate typedef, because QRgb implicitly
// includes an alpha channel whereas this type does not!
typedef quint32 code_t;
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the t stand for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shorthand for "type", common suffix in C/C++ for types and typedefs. The lowercase type names also pickup the spelling of the corresponding C++ types, i.e. optional. Lowercase type names for primitive types or PODs are also not uncommon. This keeps the definitions consistent and intuitive.


inline
QColor toQColor(RgbColor color) {
return QColor::fromRgb(color);
Copy link
Contributor

Choose a reason for hiding this comment

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

Qt will ignore the alpha bits, so you can remove the bitmask. Is there any other reason to have it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

QColor::fromRgb() implicitly adds alpha bits. But QColor::rgb() doesn't remove them. Try it without the bitmask and the tests will fail.

@Holzhaus
Copy link
Member

Can we still store the alpha bits in the Database though? Just in case we do want to allow transparency later on without rewriting the DB. We can just force them to 0xFF for now.

@uklotzde
Copy link
Contributor Author

Can we still store the alpha bits in the Database though? Just in case we do want to allow transparency later on without rewriting the DB. We can just force them to 0xFF for now.

Can we imagine any use case for alpha bits? Why should track or cue colors have differing alpha values? Doing a database migration later when we actually need this feature is much easier than complicating the code by adding them now.

@Holzhaus
Copy link
Member

Holzhaus commented Feb 2, 2020

Doing a database migration later when we actually need this feature is much easier than complicating the code by adding them now.

Yes, you're right. Something like UPDATE tracks SET color = (color ^ 0xFF000000); should work fine in case we need it. So just ignore my comment above ;-)

@uklotzde
Copy link
Contributor Author

uklotzde commented Feb 3, 2020

The track color PoC demonstrates how convenient this class and the associated utility functions are for adding (optional) color support. Anything else to do?

}

// Explicit conversion of both non-optional and optional
// RgbColor values to QColor as overloaded free functions.
Copy link
Member

Choose a reason for hiding this comment

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

Why you decide her for a free function? I think it is more QT like to have a member function for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We cannot add new functions to std::optional. The conversion functions for RgbColor and std::optional<RgbColor should use the same invocation pattern. In C++ this is only possible with a free function.

Copy link
Member

Choose a reason for hiding this comment

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

Why is std::optional relevant here?
Can't we just introduce
RgbColor::toQColor()

Copy link
Contributor Author

@uklotzde uklotzde Feb 4, 2020

Choose a reason for hiding this comment

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

No. Because std::nullopt -> QColor() (= invalid) is not covered by just RgbColor.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I don't understand the point here.
I think I have missed something.

We have here the toQColor() function taking a RgbColor. There is no std::nullopt involved.

RgbColor rgb
QColor c1 = toQColor(rgb);
QColor c2 = rgb.toQColor();

What is the issue with the later?

Copy link
Member

Choose a reason for hiding this comment

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

oh I have some ideas, hit me up on zulip

Copy link
Member

Choose a reason for hiding this comment

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

ok sounds like we have consensus on this PR!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The decision to map std::nullopt to QColor() should be done here, only once and it should be tested. Instead of hoping that developers use this convention at various places.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DRY

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same arguments apply for the conversion to QVariant. Let's not repeat those bad practices in the current code base again and again.

@daschuer
Copy link
Member

daschuer commented Feb 3, 2020

It still LGTM

@ferranpujolcamins
Copy link
Contributor

LGTM

@ywwg
Copy link
Member

ywwg commented Feb 4, 2020

Are we waiting for anything? Or can I push the big button?

@uklotzde
Copy link
Contributor Author

uklotzde commented Feb 4, 2020

Pending discussion #2472 (comment)

I strongly recommend to keep toQColor() and toQVariant() as free functions to avoid inconsistent invocation patterns. Developers should not need to think about the actual type for this use case. This is the price we have to pay for using std::optional instead of a custom class. IMHO a fair trade off.

@Holzhaus
Copy link
Member

Holzhaus commented Feb 6, 2020

Merge?

@Holzhaus
Copy link
Member

Holzhaus commented Feb 7, 2020

@uklotzde Are the any more changes needed or shall we merge?

@uklotzde
Copy link
Contributor Author

uklotzde commented Feb 8, 2020

@Holzhaus Ready. With the last commit I only added the missing tests to cover all possible conversions.

@Holzhaus Holzhaus merged commit f7f52f6 into mixxxdj:master Feb 10, 2020
@Holzhaus
Copy link
Member

Thanks!

@uklotzde uklotzde deleted the rgbcolor branch February 11, 2020 00:04
#include "util/assert.h"
#include "util/optional.h"

namespace mixxx {
Copy link
Contributor

Choose a reason for hiding this comment

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

@uklotzde Is it ok if I move this class into the Color namespace in my cue color branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please coordinate this with @Holzhaus to avoid merge conflicts. Better do it in a separate PR that is expected to be merged quickly.`

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please also note that namespace names should be lowercase:

https://google.github.io/styleguide/cppguide.html#Namespace_Names

The Color namespace violates this rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ferranpujolcamins Do we actually need a separate color namespace within mixxx for just a bunch of classes, all with color in their class name?

Copy link
Contributor

Choose a reason for hiding this comment

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

The color namespace was there to group some free functions. We can reuse it to include your class. But it doesn't really matter to me. Just let me know what you think it's best.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With argument dependent lookup and function overloading available in C++ there is no need to hide free functions in artificial namespaces. Or do I miss a use case?

We are free to chose whatever works best for us. However keep an eye on discoverability and developer experience in general.

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.

6 participants