-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Conversation
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. Did you consider to use RgbColorCode as storage variable. This has some benefit:
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? |
What does that mean, any links? Please elaborate. |
src/util/color/rgbcolor.h
Outdated
} | ||
} | ||
|
||
QColor m_color; |
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 mean this:
RgbColorCode m_color
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.
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.
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.
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.
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.
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.
See my inline comment. You may use 0xFFFFFFFF for invalid. That automatically defaults to transparent, which is what we want in most cases. |
I have turned the class upside down by replacing the internal 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. |
PS: Let's please stop the discussions about negligible performance improvements and instead get the design right and work done. |
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 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>
.
src/util/color/rgbcolor.h
Outdated
class RgbColor { | ||
public: | ||
RgbColor() { | ||
DEBUG_ASSERT(!isValid()); |
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.
Do we need the debug assertion? This is already covered by the tests.
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.
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.
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.
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.
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.
The mapping to non-optional values has to be done on the next abstraction level, not here.
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.
Debug assertions might reveal bugs that we didn't anticipate in the tests. Yes, these are helpful and intended!
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 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
(anyRgbColorCode
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 :)
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.
@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 ;)
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.
Agree, sometimes just happens that your abstraction can just be a type alias, because what you need is already there ;)
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 :(( |
@Holzhaus |
// 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; |
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 does the t
stand for?
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.
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); |
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.
Qt will ignore the alpha bits, so you can remove the bitmask. Is there any other reason to have 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.
QColor::fromRgb() implicitly adds alpha bits. But QColor::rgb() doesn't remove them. Try it without the bitmask and the tests will fail.
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 |
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. |
Yes, you're right. Something like |
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. |
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.
Why you decide her for a free function? I think it is more QT like to have a member function for this.
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.
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.
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.
Why is std::optional relevant here?
Can't we just introduce
RgbColor::toQColor()
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.
No. Because std::nullopt
-> QColor()
(= invalid) is not covered by just RgbColor
.
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.
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?
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.
oh I have some ideas, hit me up on zulip
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.
ok sounds like we have consensus on this PR!
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.
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.
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.
DRY
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.
The same arguments apply for the conversion to QVariant
. Let's not repeat those bad practices in the current code base again and again.
It still LGTM |
LGTM |
Are we waiting for anything? Or can I push the big button? |
Pending discussion #2472 (comment) I strongly recommend to keep |
Merge? |
@uklotzde Are the any more changes needed or shall we merge? |
@Holzhaus Ready. With the last commit I only added the missing tests to cover all possible conversions. |
Thanks! |
#include "util/assert.h" | ||
#include "util/optional.h" | ||
|
||
namespace mixxx { |
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.
@uklotzde Is it ok if I move this class into the Color namespace in my cue color branch?
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.
Please coordinate this with @Holzhaus to avoid merge conflicts. Better do it in a separate PR that is expected to be merged quickly.`
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.
Please also note that namespace names should be lowercase:
https://google.github.io/styleguide/cppguide.html#Namespace_Names
The Color
namespace violates this rule.
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.
@ferranpujolcamins Do we actually need a separate color
namespace within mixxx
for just a bunch of classes, all with color in their class name?
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.
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.
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.
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.
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.