From 7467a7d25cd2008b0634054f58b02a9a5c1e3d43 Mon Sep 17 00:00:00 2001 From: Michael Kazakov Date: Wed, 17 Jan 2024 21:48:51 +0000 Subject: [PATCH] Working on the presentation of the tags --- Source/Panel/Panel.xcodeproj/project.pbxproj | 22 +++- .../Panel/include/Panel/UI/TagsPresentation.h | 33 +++++ Source/Panel/source/UI/TagsPresentation.mm | 117 ++++++++++++++++++ Source/Panel/tests/UI/TagsPresentation_UT.mm | 49 ++++++++ 4 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 Source/Panel/include/Panel/UI/TagsPresentation.h create mode 100644 Source/Panel/source/UI/TagsPresentation.mm create mode 100644 Source/Panel/tests/UI/TagsPresentation_UT.mm diff --git a/Source/Panel/Panel.xcodeproj/project.pbxproj b/Source/Panel/Panel.xcodeproj/project.pbxproj index 9ba6e1c44..bf98479af 100644 --- a/Source/Panel/Panel.xcodeproj/project.pbxproj +++ b/Source/Panel/Panel.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ CF4652592699F97C0085840A /* PanelViewFieldEditor.h in Headers */ = {isa = PBXBuildFile; fileRef = CF4652582699F97C0085840A /* PanelViewFieldEditor.h */; }; CF46525D2699F9830085840A /* PanelViewFieldEditor.mm in Sources */ = {isa = PBXBuildFile; fileRef = CF46525C2699F9830085840A /* PanelViewFieldEditor.mm */; }; CF465284269A4C150085840A /* libOperations.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CF465283269A4C150085840A /* libOperations.a */; }; + CF5D1DCC2B56E5C000750174 /* TagsPresentation.mm in Sources */ = {isa = PBXBuildFile; fileRef = CF5D1DCB2B56E5C000750174 /* TagsPresentation.mm */; }; + CF5D1DCF2B58798A00750174 /* TagsPresentation_UT.mm in Sources */ = {isa = PBXBuildFile; fileRef = CF5D1DCE2B58798A00750174 /* TagsPresentation_UT.mm */; }; CF60DF252A6D3BAB00478BA0 /* libTerm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CF60DF242A6D3BAB00478BA0 /* libTerm.a */; }; CF60DF272A6D47CB00478BA0 /* ExternalTools_IT.mm in Sources */ = {isa = PBXBuildFile; fileRef = CF60DF262A6D47CB00478BA0 /* ExternalTools_IT.mm */; }; CF739C3D295644CD004758C5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CF739C3B295644CD004758C5 /* Localizable.strings */; }; @@ -111,6 +113,9 @@ CF4652582699F97C0085840A /* PanelViewFieldEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PanelViewFieldEditor.h; path = include/Panel/PanelViewFieldEditor.h; sourceTree = ""; }; CF46525C2699F9830085840A /* PanelViewFieldEditor.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = PanelViewFieldEditor.mm; path = source/PanelViewFieldEditor.mm; sourceTree = ""; }; CF465283269A4C150085840A /* libOperations.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libOperations.a; sourceTree = BUILT_PRODUCTS_DIR; }; + CF5D1DCA2B56E5AE00750174 /* TagsPresentation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TagsPresentation.h; path = include/Panel/UI/TagsPresentation.h; sourceTree = ""; }; + CF5D1DCB2B56E5C000750174 /* TagsPresentation.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TagsPresentation.mm; path = source/UI/TagsPresentation.mm; sourceTree = ""; }; + CF5D1DCE2B58798A00750174 /* TagsPresentation_UT.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TagsPresentation_UT.mm; path = tests/UI/TagsPresentation_UT.mm; sourceTree = ""; }; CF60DF242A6D3BAB00478BA0 /* libTerm.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libTerm.a; sourceTree = BUILT_PRODUCTS_DIR; }; CF60DF262A6D47CB00478BA0 /* ExternalTools_IT.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ExternalTools_IT.mm; path = tests/ExternalTools_IT.mm; sourceTree = ""; }; CF739C3C295644CD004758C5 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; @@ -192,9 +197,10 @@ CF0B0403281F013800076FDF /* UI */ = { isa = PBXGroup; children = ( - CF0B0404281F014800076FDF /* SelectionWithMaskPopupViewController.h */, CFCB68FF28968C3600086E40 /* PanelViewPresentationItemsColoringFilter.h */, CFCB68FE28968C3600086E40 /* PanelViewPresentationItemsColoringFilterPersistence.h */, + CF0B0404281F014800076FDF /* SelectionWithMaskPopupViewController.h */, + CF5D1DCA2B56E5AE00750174 /* TagsPresentation.h */, ); name = UI; sourceTree = ""; @@ -202,9 +208,18 @@ CF0B0406281F015A00076FDF /* UI */ = { isa = PBXGroup; children = ( - CF0B0407281F016300076FDF /* SelectionWithMaskPopupViewController.mm */, CFCB690128968C4800086E40 /* PanelViewPresentationItemsColoringFilter.mm */, CFCB690028968C4800086E40 /* PanelViewPresentationItemsColoringFilterPersistence.mm */, + CF0B0407281F016300076FDF /* SelectionWithMaskPopupViewController.mm */, + CF5D1DCB2B56E5C000750174 /* TagsPresentation.mm */, + ); + name = UI; + sourceTree = ""; + }; + CF5D1DCD2B58797500750174 /* UI */ = { + isa = PBXGroup; + children = ( + CF5D1DCE2B58798A00750174 /* TagsPresentation_UT.mm */, ); name = UI; sourceTree = ""; @@ -296,6 +311,7 @@ CFF33FEF25569DBE00B3C92C /* Tests */ = { isa = PBXGroup; children = ( + CF5D1DCD2B58797500750174 /* UI */, CF349B0D25FCAE9B009735DC /* Comparators_UT.mm */, CF60DF262A6D47CB00478BA0 /* ExternalTools_IT.mm */, CF22060827B9B73C008EDE3A /* ExternalTools_UT.mm */, @@ -480,6 +496,7 @@ CFCB690228968C4800086E40 /* PanelViewPresentationItemsColoringFilterPersistence.mm in Sources */, CF0B040B281F027900076FDF /* Internal.mm in Sources */, CF46522D268D16500085840A /* Log.cpp in Sources */, + CF5D1DCC2B56E5C000750174 /* TagsPresentation.mm in Sources */, CFA89D842804C87700BEA127 /* FindFilesData.cpp in Sources */, CFF33FBE255695BF00B3C92C /* PanelDataExternalEntryKey.cpp in Sources */, CFF33FA02556948900B3C92C /* PanelDataSortMode.cpp in Sources */, @@ -507,6 +524,7 @@ CF3ED50925860E1000D67AF2 /* QuickSearch_UT.mm in Sources */, CF96DC7629CF4610003EC4EB /* ItemVolatileData_UT.mm in Sources */, CFF3401625569EC600B3C92C /* PanelData_UT.mm in Sources */, + CF5D1DCF2B58798A00750174 /* TagsPresentation_UT.mm in Sources */, CFF33FF225569DF300B3C92C /* Tests.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Source/Panel/include/Panel/UI/TagsPresentation.h b/Source/Panel/include/Panel/UI/TagsPresentation.h new file mode 100644 index 000000000..387671bb2 --- /dev/null +++ b/Source/Panel/include/Panel/UI/TagsPresentation.h @@ -0,0 +1,33 @@ +// Copyright (C) 2024 Michael Kazakov. Subject to GNU General Public License version 3. +#pragma once +#include +#include +#include + +namespace nc::panel { + +struct TrailingTagsInplaceDisplay { + static constexpr int MaxDrawn = 3; + static constexpr int Diameter = 10; + static constexpr int Step = 5; + static constexpr int Margin = 8; + + struct Geom { + int width = 0; + int margin = 0; + }; + + // Provides informations about required space to draw the specified set of tags + static Geom Place(std::span _tags) noexcept; + + // Draws the specified set of tags in the current context. + // if _accent is given it is used to stroke the tags, natural stroke colors is used otherwise. + // background colors is used when more than one tag is drawn. + static void Draw(double _offset_x, + double _view_height, + std::span _tags, + NSColor *_accent, + NSColor *_background) noexcept; +}; + +} // namespace nc::panel diff --git a/Source/Panel/source/UI/TagsPresentation.mm b/Source/Panel/source/UI/TagsPresentation.mm new file mode 100644 index 000000000..7ca17b524 --- /dev/null +++ b/Source/Panel/source/UI/TagsPresentation.mm @@ -0,0 +1,117 @@ +// Copyright (C) 2024 Michael Kazakov. Subject to GNU General Public License version 3. +#include "UI/TagsPresentation.h" +#include +#include +#include +#include +#include +#include + +namespace nc::panel { + +static NSColor *Saturate(NSColor *_color) noexcept +{ + double factor = 1.5; + double hue, saturation, brightness, alpha; + [_color getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]; + return [NSColor colorWithCalibratedHue:hue + saturation:std::min(1.0, saturation * factor) + brightness:brightness + alpha:alpha]; +} + +// fill, stroke +static std::pair Color(utility::Tags::Color _color) noexcept +{ + assert(std::to_underlying(_color) < 8); + [[clang::no_destroy]] static std::array fill_colors; + [[clang::no_destroy]] static std::array stroke_colors; + static std::once_flag once; + std::call_once(once, [] { + // TODO: explicit components for stroke + constexpr int components[8][4] = {{0, 0, 0, 0}, + {147, 147, 151, 255}, + {71, 208, 83, 255}, + {174, 88, 220, 255}, + {42, 125, 252, 255}, + {252, 205, 40, 255}, + {252, 73, 72, 255}, + {252, 154, 40, 255}}; + + for( size_t i = 0; i < 8; ++i ) { + fill_colors[i] = [NSColor colorWithCalibratedRed:components[i][0] / 255. + green:components[i][1] / 255. + blue:components[i][2] / 255. + alpha:components[i][3] / 255.]; + stroke_colors[i] = Saturate(fill_colors[i]); + } + }); + auto idx = std::to_underlying(_color); + return {fill_colors[idx], stroke_colors[idx]}; +} + +TrailingTagsInplaceDisplay::Geom +TrailingTagsInplaceDisplay::Place(const std::span _tags) noexcept +{ + auto count = std::ranges::count_if(_tags, [](auto &_tag) { return _tag.Color() != utility::Tags::Color::None; }); + if( count == 0 ) + return {}; + return {Diameter + (std::min(static_cast(count), MaxDrawn) - 1) * Step, Margin}; +} + +void TrailingTagsInplaceDisplay::Draw(const double _offset_x, + const double _view_height, + const std::span _tags, + NSColor *_accent, + NSColor *_background) noexcept +{ + if( _tags.empty() ) + return; + + // Take up to MaxDrawn tags that have a color other than None + std::array colors_to_draw; + size_t num_colors_to_draw = 0; + + for( auto it = _tags.rbegin(); it != _tags.rend() && num_colors_to_draw < MaxDrawn; ++it ) { + if( it->Color() != utility::Tags::Color::None ) + colors_to_draw[num_colors_to_draw++] = it->Color(); + } + + if( num_colors_to_draw == 0 ) + return; + + NSGraphicsContext *currentContext = [NSGraphicsContext currentContext]; + [currentContext saveGraphicsState]; + + constexpr double radius = static_cast(Diameter / 2); + constexpr double spacing = static_cast(Step); + + for( ssize_t i = num_colors_to_draw - 1; i >= 0; --i ) { + auto colors = Color(colors_to_draw[i]); + [colors.first setFill]; + if( _accent ) + [_accent setStroke]; + else + [colors.second setStroke]; + + NSPoint center = NSMakePoint(_offset_x + i * spacing, _view_height / 2.); + + NSBezierPath *circle = [NSBezierPath bezierPath]; + [circle appendBezierPathWithArcWithCenter:center radius:radius startAngle:0 endAngle:360]; + [circle fill]; + [circle setLineWidth:1.]; + [circle stroke]; + + if( i < static_cast(num_colors_to_draw) - 1 ) { + NSBezierPath *shadow = [NSBezierPath bezierPath]; + [shadow appendBezierPathWithArcWithCenter:center radius:radius + 1. startAngle:0 endAngle:360]; + [_background setStroke]; + [shadow setLineWidth:1.]; + [shadow stroke]; + } + } + + [currentContext restoreGraphicsState]; +} + +} diff --git a/Source/Panel/tests/UI/TagsPresentation_UT.mm b/Source/Panel/tests/UI/TagsPresentation_UT.mm new file mode 100644 index 000000000..e672e2b90 --- /dev/null +++ b/Source/Panel/tests/UI/TagsPresentation_UT.mm @@ -0,0 +1,49 @@ +// Copyright (C) 2024 Michael Kazakov. Subject to GNU General Public License version 3. +#include +#include +#include +#include "UI/TagsPresentation.h" +#include "../Tests.h" + +#define PREFIX "TagsPresentation " + +using namespace nc; +using namespace nc::base; +using namespace nc::panel; +using utility::Tags; + +TEST_CASE(PREFIX "TrailingTagsInplaceDisplay::Place") +{ + using Tag = Tags::Tag; + using C = Tags::Color; + constexpr int D = TrailingTagsInplaceDisplay::Diameter; + constexpr int S = TrailingTagsInplaceDisplay::Step; + constexpr int M = TrailingTagsInplaceDisplay::Margin; + const std::string l = "doesnt matter"; + struct TC { + std::vector tags; + int exp_width = 0; + int exp_margin = 0; + } tcs[] = { + {{}, 0, 0}, + {{Tag(&l, C::None)}, 0, 0}, + {{Tag(&l, C::Blue)}, D, M}, + {{Tag(&l, C::None), Tag(&l, C::None)}, 0, 0}, + {{Tag(&l, C::None), Tag(&l, C::None), Tag(&l, C::None)}, 0, 0}, + {{Tag(&l, C::None), Tag(&l, C::Blue)}, D, M}, + {{Tag(&l, C::Blue), Tag(&l, C::None)}, D, M}, + {{Tag(&l, C::None), Tag(&l, C::Blue), Tag(&l, C::None)}, D, M}, + {{Tag(&l, C::Blue), Tag(&l, C::Blue)}, D + S, M}, + {{Tag(&l, C::Blue), Tag(&l, C::None), Tag(&l, C::Blue)}, D + S, M}, + {{Tag(&l, C::None), Tag(&l, C::Blue), Tag(&l, C::Blue)}, D + S, M}, + {{Tag(&l, C::Blue), Tag(&l, C::Blue), Tag(&l, C::None)}, D + S, M}, + {{Tag(&l, C::None), Tag(&l, C::Blue), Tag(&l, C::Blue), Tag(&l, C::None)}, D + S, M}, + {{Tag(&l, C::Blue), Tag(&l, C::Blue), Tag(&l, C::Blue)}, D + S + S, M}, + {{Tag(&l, C::Blue), Tag(&l, C::Blue), Tag(&l, C::Blue), Tag(&l, C::Blue)}, D + S + S, M}, + {{Tag(&l, C::Blue), Tag(&l, C::None), Tag(&l, C::Blue), Tag(&l, C::Blue), Tag(&l, C::Blue)}, D + S + S, M}}; + for( const auto &tc : tcs ) { + auto geom = TrailingTagsInplaceDisplay::Place(tc.tags); + CHECK(geom.width == tc.exp_width); + CHECK(geom.margin == tc.exp_margin); + } +}