diff --git a/doc/api.md b/doc/api.md index e83eb8f6ddc4..1f1d6e330ba4 100644 --- a/doc/api.md +++ b/doc/api.md @@ -580,7 +580,7 @@ performance bottleneck. `fmt/color.h` provides support for terminal color and text style output. -::: print(const text_style&, format_string, T&&...) +::: print(text_style, format_string, T&&...) ::: fg(detail::color_type) diff --git a/include/fmt/color.h b/include/fmt/color.h index 2faaf3a067a6..b65b42d66aff 100644 --- a/include/fmt/color.h +++ b/include/fmt/color.h @@ -205,97 +205,135 @@ struct rgb { namespace detail { -// color is a struct of either a rgb color or a terminal color. +// a bit-packed variant of an RGB color, a terminal color, or unset color. +// see text_style for the bit-packing scheme. struct color_type { - FMT_CONSTEXPR color_type() noexcept : is_rgb(), value{} {} - FMT_CONSTEXPR color_type(color rgb_color) noexcept : is_rgb(true), value{} { - value.rgb_color = static_cast(rgb_color); - } - FMT_CONSTEXPR color_type(rgb rgb_color) noexcept : is_rgb(true), value{} { - value.rgb_color = (static_cast(rgb_color.r) << 16) | - (static_cast(rgb_color.g) << 8) | rgb_color.b; - } + FMT_CONSTEXPR color_type() noexcept = default; + FMT_CONSTEXPR color_type(color rgb_color) noexcept + : value_(static_cast(rgb_color) | (1 << 24)) {} + FMT_CONSTEXPR color_type(rgb rgb_color) noexcept + : color_type(static_cast( + (static_cast(rgb_color.r) << 16) | + (static_cast(rgb_color.g) << 8) | rgb_color.b)) {} FMT_CONSTEXPR color_type(terminal_color term_color) noexcept - : is_rgb(), value{} { - value.term_color = static_cast(term_color); + : value_(static_cast(term_color) | (3 << 24)) {} + + FMT_CONSTEXPR auto is_terminal_color() const noexcept -> bool { + return (value_ & (1 << 25)) != 0; + } + + FMT_CONSTEXPR auto value() const noexcept -> uint32_t { + return value_ & 0xFFFFFF; } - bool is_rgb; - union color_union { - uint8_t term_color; - uint32_t rgb_color; - } value; + + FMT_CONSTEXPR color_type(uint32_t value) noexcept : value_(value) {} + + uint32_t value_{}; }; } // namespace detail /// A text style consisting of foreground and background colors and emphasis. class text_style { + // The information is packed as follows: + // ┌──┐ + // │ 0│─┐ + // │..│ ├── foreground color value + // │23│─┘ + // ├──┤ + // │24│─┬── discriminator for the above value. 00 if unset, 01 if it's + // │25│─┘ an RGB color, or 11 if it's a terminal color (10 is unused) + // ├──┤ + // │26│──── overflow bit, always zero (see below) + // ├──┤ + // │27│─┐ + // │..│ │ + // │50│ │ + // ├──┤ │ + // │51│ ├── background color (same format as the foreground color) + // │52│ │ + // ├──┤ │ + // │53│─┘ + // ├──┤ + // │54│─┐ + // │..│ ├── emphases + // │61│─┘ + // ├──┤ + // │62│─┬── unused + // │63│─┘ + // └──┘ + // The overflow bits are there to make operator|= efficient. + // When ORing, we must throw if, for either the foreground or background, + // one style specifies a terminal color and the other specifies any color + // (terminal or RGB); in other words, if one discriminator is 11 and the + // other is 11 or 01. + // + // We do that check by adding the styles. Consider what adding does to each + // possible pair of discriminators: + // 00 + 00 = 000 + // 01 + 00 = 001 + // 11 + 00 = 011 + // 01 + 01 = 010 + // 11 + 01 = 100 (!!) + // 11 + 11 = 110 (!!) + // In the last two cases, the ones we want to catch, the third bit——the + // overflow bit——is set. Bingo. + // + // We must take into account the possible carry bit from the bits + // before the discriminator. The only potentially problematic case is + // 11 + 00 = 011 (a carry bit would make it 100, not good!), but a carry + // bit is impossible in that case, because 00 (unset color) means the + // 24 bits that precede the discriminator are all zero. + // + // This test can be applied to both colors simultaneously. + public: FMT_CONSTEXPR text_style(emphasis em = emphasis()) noexcept - : set_foreground_color(), set_background_color(), ems(em) {} - - FMT_CONSTEXPR auto operator|=(const text_style& rhs) -> text_style& { - if (!set_foreground_color) { - set_foreground_color = rhs.set_foreground_color; - foreground_color = rhs.foreground_color; - } else if (rhs.set_foreground_color) { - if (!foreground_color.is_rgb || !rhs.foreground_color.is_rgb) - report_error("can't OR a terminal color"); - foreground_color.value.rgb_color |= rhs.foreground_color.value.rgb_color; - } + : style_(static_cast(em) << 54) {} - if (!set_background_color) { - set_background_color = rhs.set_background_color; - background_color = rhs.background_color; - } else if (rhs.set_background_color) { - if (!background_color.is_rgb || !rhs.background_color.is_rgb) - report_error("can't OR a terminal color"); - background_color.value.rgb_color |= rhs.background_color.value.rgb_color; - } - - ems = static_cast(static_cast(ems) | - static_cast(rhs.ems)); + FMT_CONSTEXPR auto operator|=(text_style rhs) -> text_style& { + if (((style_ + rhs.style_) & ((1ULL << 26) | (1ULL << 53))) != 0) + report_error("can't OR a terminal color"); + style_ |= rhs.style_; return *this; } - friend FMT_CONSTEXPR auto operator|(text_style lhs, const text_style& rhs) + friend FMT_CONSTEXPR auto operator|(text_style lhs, text_style rhs) -> text_style { return lhs |= rhs; } + FMT_CONSTEXPR auto operator==(text_style rhs) const noexcept -> bool { + return style_ == rhs.style_; + } + + FMT_CONSTEXPR auto operator!=(text_style rhs) const noexcept -> bool { + return !(*this == rhs); + } + FMT_CONSTEXPR auto has_foreground() const noexcept -> bool { - return set_foreground_color; + return (style_ & (1 << 24)) != 0; } FMT_CONSTEXPR auto has_background() const noexcept -> bool { - return set_background_color; + return (style_ & (1ULL << 51)) != 0; } FMT_CONSTEXPR auto has_emphasis() const noexcept -> bool { - return static_cast(ems) != 0; + return (style_ >> 54) != 0; } FMT_CONSTEXPR auto get_foreground() const noexcept -> detail::color_type { FMT_ASSERT(has_foreground(), "no foreground specified for this style"); - return foreground_color; + return style_ & 0x3FFFFFF; } FMT_CONSTEXPR auto get_background() const noexcept -> detail::color_type { FMT_ASSERT(has_background(), "no background specified for this style"); - return background_color; + return (style_ >> 27) & 0x3FFFFFF; } FMT_CONSTEXPR auto get_emphasis() const noexcept -> emphasis { FMT_ASSERT(has_emphasis(), "no emphasis specified for this style"); - return ems; + return static_cast(style_ >> 54); } private: - FMT_CONSTEXPR text_style(bool is_foreground, - detail::color_type text_color) noexcept - : set_foreground_color(), set_background_color(), ems() { - if (is_foreground) { - foreground_color = text_color; - set_foreground_color = true; - } else { - background_color = text_color; - set_background_color = true; - } - } + FMT_CONSTEXPR text_style(uint64_t style) noexcept : style_(style) {} friend FMT_CONSTEXPR auto fg(detail::color_type foreground) noexcept -> text_style; @@ -303,23 +341,19 @@ class text_style { friend FMT_CONSTEXPR auto bg(detail::color_type background) noexcept -> text_style; - detail::color_type foreground_color; - detail::color_type background_color; - bool set_foreground_color; - bool set_background_color; - emphasis ems; + uint64_t style_{}; }; /// Creates a text style from the foreground (text) color. FMT_CONSTEXPR inline auto fg(detail::color_type foreground) noexcept -> text_style { - return text_style(true, foreground); + return foreground.value_; } /// Creates a text style from the background color. FMT_CONSTEXPR inline auto bg(detail::color_type background) noexcept -> text_style { - return text_style(false, background); + return static_cast(background.value_) << 27; } FMT_CONSTEXPR inline auto operator|(emphasis lhs, emphasis rhs) noexcept @@ -334,9 +368,9 @@ template struct ansi_color_escape { const char* esc) noexcept { // If we have a terminal color, we need to output another escape code // sequence. - if (!text_color.is_rgb) { + if (text_color.is_terminal_color()) { bool is_background = esc == string_view("\x1b[48;2;"); - uint32_t value = text_color.value.term_color; + uint32_t value = text_color.value(); // Background ASCII codes are the same as the foreground ones but with // 10 more. if (is_background) value += 10u; @@ -360,7 +394,7 @@ template struct ansi_color_escape { for (int i = 0; i < 7; i++) { buffer[i] = static_cast(esc[i]); } - rgb color(text_color.value.rgb_color); + rgb color(text_color.value()); to_esc(color.r, buffer + 7, ';'); to_esc(color.g, buffer + 11, ';'); to_esc(color.b, buffer + 15, 'm'); @@ -441,32 +475,26 @@ template struct styled_arg : view { }; template -void vformat_to(buffer& buf, const text_style& ts, - basic_string_view fmt, +void vformat_to(buffer& buf, text_style ts, basic_string_view fmt, basic_format_args> args) { - bool has_style = false; if (ts.has_emphasis()) { - has_style = true; auto emphasis = make_emphasis(ts.get_emphasis()); buf.append(emphasis.begin(), emphasis.end()); } if (ts.has_foreground()) { - has_style = true; auto foreground = make_foreground_color(ts.get_foreground()); buf.append(foreground.begin(), foreground.end()); } if (ts.has_background()) { - has_style = true; auto background = make_background_color(ts.get_background()); buf.append(background.begin(), background.end()); } vformat_to(buf, fmt, args); - if (has_style) reset_color(buf); + if (ts != text_style{}) reset_color(buf); } } // namespace detail -inline void vprint(FILE* f, const text_style& ts, string_view fmt, - format_args args) { +inline void vprint(FILE* f, text_style ts, string_view fmt, format_args args) { auto buf = memory_buffer(); detail::vformat_to(buf, ts, fmt, args); print(f, FMT_STRING("{}"), string_view(buf.begin(), buf.size())); @@ -482,8 +510,7 @@ inline void vprint(FILE* f, const text_style& ts, string_view fmt, * "Elapsed time: {0:.2f} seconds", 1.23); */ template -void print(FILE* f, const text_style& ts, format_string fmt, - T&&... args) { +void print(FILE* f, text_style ts, format_string fmt, T&&... args) { vprint(f, ts, fmt.str, vargs{{args...}}); } @@ -497,11 +524,11 @@ void print(FILE* f, const text_style& ts, format_string fmt, * "Elapsed time: {0:.2f} seconds", 1.23); */ template -void print(const text_style& ts, format_string fmt, T&&... args) { +void print(text_style ts, format_string fmt, T&&... args) { return print(stdout, ts, fmt, std::forward(args)...); } -inline auto vformat(const text_style& ts, string_view fmt, format_args args) +inline auto vformat(text_style ts, string_view fmt, format_args args) -> std::string { auto buf = memory_buffer(); detail::vformat_to(buf, ts, fmt, args); @@ -521,7 +548,7 @@ inline auto vformat(const text_style& ts, string_view fmt, format_args args) * ``` */ template -inline auto format(const text_style& ts, format_string fmt, T&&... args) +inline auto format(text_style ts, format_string fmt, T&&... args) -> std::string { return fmt::vformat(ts, fmt.str, vargs{{args...}}); } @@ -529,8 +556,8 @@ inline auto format(const text_style& ts, format_string fmt, T&&... args) /// Formats a string with the given text_style and writes the output to `out`. template ::value)> -auto vformat_to(OutputIt out, const text_style& ts, string_view fmt, - format_args args) -> OutputIt { +auto vformat_to(OutputIt out, text_style ts, string_view fmt, format_args args) + -> OutputIt { auto&& buf = detail::get_buffer(out); detail::vformat_to(buf, ts, fmt, args); return detail::get_iterator(buf, out); @@ -548,8 +575,8 @@ auto vformat_to(OutputIt out, const text_style& ts, string_view fmt, */ template ::value)> -inline auto format_to(OutputIt out, const text_style& ts, - format_string fmt, T&&... args) -> OutputIt { +inline auto format_to(OutputIt out, text_style ts, format_string fmt, + T&&... args) -> OutputIt { return vformat_to(out, ts, fmt.str, vargs{{args...}}); } diff --git a/include/fmt/xchar.h b/include/fmt/xchar.h index 9f7f889d64d5..d9590109b811 100644 --- a/include/fmt/xchar.h +++ b/include/fmt/xchar.h @@ -322,7 +322,7 @@ template void println(wformat_string fmt, T&&... args) { return print(L"{}\n", fmt::format(fmt, std::forward(args)...)); } -inline auto vformat(const text_style& ts, wstring_view fmt, wformat_args args) +inline auto vformat(text_style ts, wstring_view fmt, wformat_args args) -> std::wstring { auto buf = wmemory_buffer(); detail::vformat_to(buf, ts, fmt, args); @@ -330,19 +330,19 @@ inline auto vformat(const text_style& ts, wstring_view fmt, wformat_args args) } template -inline auto format(const text_style& ts, wformat_string fmt, T&&... args) +inline auto format(text_style ts, wformat_string fmt, T&&... args) -> std::wstring { return fmt::vformat(ts, fmt, fmt::make_wformat_args(args...)); } template -FMT_DEPRECATED void print(std::FILE* f, const text_style& ts, - wformat_string fmt, const T&... args) { +FMT_DEPRECATED void print(std::FILE* f, text_style ts, wformat_string fmt, + const T&... args) { vprint(f, ts, fmt, fmt::make_wformat_args(args...)); } template -FMT_DEPRECATED void print(const text_style& ts, wformat_string fmt, +FMT_DEPRECATED void print(text_style ts, wformat_string fmt, const T&... args) { return print(stdout, ts, fmt, args...); } diff --git a/test/color-test.cc b/test/color-test.cc index c2ba13a977db..73c9eed037f6 100644 --- a/test/color-test.cc +++ b/test/color-test.cc @@ -9,11 +9,66 @@ #include // std::back_inserter -#include "gtest-extra.h" // EXPECT_WRITE +#include "gtest-extra.h" // EXPECT_WRITE, EXPECT_THROW_MSG + +TEST(color_test, text_style) { + EXPECT_FALSE(fmt::text_style{}.has_foreground()); + EXPECT_FALSE(fmt::text_style{}.has_background()); + EXPECT_FALSE(fmt::text_style{}.has_emphasis()); + + EXPECT_TRUE(fg(fmt::rgb(0)).has_foreground()); + EXPECT_FALSE(fg(fmt::rgb(0)).has_background()); + EXPECT_FALSE(fg(fmt::rgb(0)).has_emphasis()); + EXPECT_TRUE(bg(fmt::rgb(0)).has_background()); + EXPECT_FALSE(bg(fmt::rgb(0)).has_foreground()); + EXPECT_FALSE(bg(fmt::rgb(0)).has_emphasis()); + + EXPECT_TRUE( + (fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_foreground()); + EXPECT_TRUE( + (fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_background()); + EXPECT_FALSE( + (fg(fmt::rgb(0xFFFFFF)) | bg(fmt::rgb(0xFFFFFF))).has_emphasis()); + + EXPECT_EQ(fg(fmt::rgb(0x000000)) | fg(fmt::rgb(0x000000)), + fg(fmt::rgb(0x000000))); + EXPECT_EQ(fg(fmt::rgb(0x00000F)) | fg(fmt::rgb(0x00000F)), + fg(fmt::rgb(0x00000F))); + EXPECT_EQ(fg(fmt::rgb(0xC0F000)) | fg(fmt::rgb(0x000FEE)), + fg(fmt::rgb(0xC0FFEE))); + + EXPECT_THROW_MSG( + fg(fmt::terminal_color::black) | fg(fmt::terminal_color::black), + fmt::format_error, "can't OR a terminal color"); + EXPECT_THROW_MSG( + fg(fmt::terminal_color::black) | fg(fmt::terminal_color::white), + fmt::format_error, "can't OR a terminal color"); + EXPECT_THROW_MSG( + bg(fmt::terminal_color::black) | bg(fmt::terminal_color::black), + fmt::format_error, "can't OR a terminal color"); + EXPECT_THROW_MSG( + bg(fmt::terminal_color::black) | bg(fmt::terminal_color::white), + fmt::format_error, "can't OR a terminal color"); + EXPECT_THROW_MSG(fg(fmt::terminal_color::black) | fg(fmt::color::black), + fmt::format_error, "can't OR a terminal color"); + EXPECT_THROW_MSG(bg(fmt::terminal_color::black) | bg(fmt::color::black), + fmt::format_error, "can't OR a terminal color"); + + EXPECT_NO_THROW(fg(fmt::terminal_color::white) | + bg(fmt::terminal_color::white)); + EXPECT_NO_THROW(fg(fmt::terminal_color::white) | bg(fmt::rgb(0xFFFFFF))); + EXPECT_NO_THROW(fg(fmt::terminal_color::white) | fmt::text_style{}); + EXPECT_NO_THROW(bg(fmt::terminal_color::white) | fmt::text_style{}); +} TEST(color_test, format) { + EXPECT_EQ(fmt::format(fmt::text_style{}, "no style"), "no style"); EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 20, 30)), "rgb(255,20,30)"), "\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m"); + EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 0, 0)) | fg(fmt::rgb(0, 20, 30)), "rgb(255,20,30)"), + "\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m"); + EXPECT_EQ(fmt::format(fg(fmt::rgb(0, 0, 0)) | fg(fmt::rgb(0, 0, 0)), "rgb(0,0,0)"), + "\x1b[38;2;000;000;000mrgb(0,0,0)\x1b[0m"); EXPECT_EQ(fmt::format(fg(fmt::color::blue), "blue"), "\x1b[38;2;000;000;255mblue\x1b[0m"); EXPECT_EQ(