feat: TinySVG + regex icon

This commit is contained in:
relikd
2025-06-18 15:40:57 +02:00
parent 86f5abde0c
commit df0b5b1c91
5 changed files with 223 additions and 16 deletions

View File

@@ -12,6 +12,7 @@
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195885218E1BDB00581B79 /* NSMenu+Ext.m */; }; 54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195885218E1BDB00581B79 /* NSMenu+Ext.m */; };
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 541C67C22255470B004D2CE6 /* SettingsAppearance.m */; }; 541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 541C67C22255470B004D2CE6 /* SettingsAppearance.m */; };
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; }; 54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
54229F552E02491A0019ACB0 /* TinySVG.m in Sources */ = {isa = PBXBuildFile; fileRef = 54229F542E02491A0019ACB0 /* TinySVG.m */; };
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; }; 544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; }; 544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 5450100F230E9C8600F0B165 /* FeedDownload.m */; }; 54501010230E9C8600F0B165 /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 5450100F230E9C8600F0B165 /* FeedDownload.m */; };
@@ -127,6 +128,8 @@
541C67C22255470B004D2CE6 /* SettingsAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsAppearance.m; sourceTree = "<group>"; }; 541C67C22255470B004D2CE6 /* SettingsAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsAppearance.m; sourceTree = "<group>"; };
54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; }; 54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; };
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; }; 54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
54229F532E02491A0019ACB0 /* TinySVG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TinySVG.h; sourceTree = "<group>"; };
54229F542E02491A0019ACB0 /* TinySVG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TinySVG.m; sourceTree = "<group>"; };
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; }; 544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; }; 544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; }; 544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
@@ -429,6 +432,8 @@
54209E932117325100F3B5EF /* DrawImage.m */, 54209E932117325100F3B5EF /* DrawImage.m */,
54910065233A4D4000858AE2 /* URLScheme.h */, 54910065233A4D4000858AE2 /* URLScheme.h */,
54910066233A4D4000858AE2 /* URLScheme.m */, 54910066233A4D4000858AE2 /* URLScheme.m */,
54229F532E02491A0019ACB0 /* TinySVG.h */,
54229F542E02491A0019ACB0 /* TinySVG.m */,
); );
path = Helper; path = Helper;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -628,6 +633,7 @@
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */, 54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */,
54910067233A4D4000858AE2 /* URLScheme.m in Sources */, 54910067233A4D4000858AE2 /* URLScheme.m in Sources */,
54F6025D21C1D4170006D338 /* OpmlFile.m in Sources */, 54F6025D21C1D4170006D338 /* OpmlFile.m in Sources */,
54229F552E02491A0019ACB0 /* TinySVG.m in Sources */,
5496B511214D6275003ED4ED /* UserPrefs.m in Sources */, 5496B511214D6275003ED4ED /* UserPrefs.m in Sources */,
546A6A2F22C585580034E806 /* SettingsAboutView.m in Sources */, 546A6A2F22C585580034E806 /* SettingsAboutView.m in Sources */,
54B517072270E990006C1B29 /* NSView+Ext.m in Sources */, 54B517072270E990006C1B29 /* NSView+Ext.m in Sources */,

View File

@@ -35,6 +35,8 @@ static NSImageName const RSSImageMenuBarIconActive = @"RSSImageMenuBarIconActive
static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused"; static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused";
/// Menu item, unread state icon (blue dot) /// Menu item, unread state icon (blue dot)
static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread"; static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread";
/// Feed edit, regex editor icon @c "(.*)"
static NSImageName const RSSImageRegexIcon = @"RSSImageRegexIcon";
#pragma mark - NSNotificationName constants #pragma mark - NSNotificationName constants

View File

@@ -1,6 +1,7 @@
#import "DrawImage.h" #import "DrawImage.h"
#import "Constants.h" #import "Constants.h"
#import "NSColor+Ext.h" #import "NSColor+Ext.h"
#import "TinySVG.h"
@implementation DrawSeparator @implementation DrawSeparator
@@ -125,7 +126,6 @@ static inline void AddGroupIconPath(CGContextRef c, CGFloat size, BOOL showBackg
CGPathRelease(lower); CGPathRelease(lower);
} }
/** /**
Create @c CGPath for RSS icon; a circle in the lower left bottom and two radio waves going outwards. Create @c CGPath for RSS icon; a circle in the lower left bottom and two radio waves going outwards.
@param connection If @c NO, draw only one radio wave and a pause icon in the upper right @param connection If @c NO, draw only one radio wave and a pause icon in the upper right
@@ -146,22 +146,9 @@ static inline void AddRSSIconPath(CGContextRef c, CGFloat size, BOOL connection)
} }
#pragma mark - Icon Background Generators #pragma mark - Icon Background
/// Create @c CGPath with rounded corners (optional). @param roundness Value between @c 0.0 and @c 1.0
static void AddRoundedBackgroundPath(CGContextRef c, CGRect r, CGFloat roundness) {
const CGFloat corner = ShorterSide(r.size) * (roundness / 2.0);
if (corner > 0) {
CGMutablePathRef pth = CGPathCreateMutable();
CGPathAddRoundedRect(pth, NULL, r, corner, corner);
CGContextAddPath(c, pth);
CGPathRelease(pth);
} else {
CGContextAddRect(c, r);
}
}
/// Insert and draw linear gradient with @c color saturation @c ±0.3 /// Insert and draw linear gradient with @c color saturation @c ±0.3
static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) { static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) {
CGFloat h = 0, s = 1, b = 1, a = 1; CGFloat h = 0, s = 1, b = 1, a = 1;
@@ -190,6 +177,12 @@ static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) {
#pragma mark - CGContext Drawing & Manipulation #pragma mark - CGContext Drawing & Manipulation
/// Flip coordinate system
static void FlipCoordinateSystem(CGContextRef c, CGFloat height) {
CGContextTranslateCTM(c, 0, height);
CGContextScaleCTM(c, 1, -1);
}
/// Scale and translate context to the center with respect to the new scale. If @c width @c != @c length align top left. /// Scale and translate context to the center with respect to the new scale. If @c width @c != @c length align top left.
static void SetContentScale(CGContextRef c, CGSize size, CGFloat scale) { static void SetContentScale(CGContextRef c, CGSize size, CGFloat scale) {
const CGFloat s = ShorterSide(size); const CGFloat s = ShorterSide(size);
@@ -204,7 +197,7 @@ static void DrawRoundedFrame(CGContextRef c, CGRect r, CGColorRef color, BOOL ba
CGContextSetStrokeColorWithColor(c, color); CGContextSetStrokeColorWithColor(c, color);
CGFloat contentScale = defaultScale; CGFloat contentScale = defaultScale;
if (background) { if (background) {
AddRoundedBackgroundPath(c, r, corner); svgAddRect(c, 1, r, ShorterSide(r.size) * corner/2);
if (scaling != 0.0) if (scaling != 0.0)
contentScale *= scaling; contentScale *= scaling;
} }
@@ -277,6 +270,31 @@ static void DrawUnreadIcon(CGRect r, NSColor *color) {
CGPathRelease(path); CGPathRelease(path);
} }
/// Draw "(.*)" as vector path
static void DrawRegexIcon(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
svgAddRect(c, 1, r, .2 * size);
CGContextSetFillColorWithColor(c, NSColor.redColor.CGColor);
CGContextFillPath(c);
// SVG files use bottom-left corner coordinate system. Quartz uses top-left.
FlipCoordinateSystem(c, r.size.height);
SetContentScale(c, r.size, 0.8);
// "("
svgAddPath(c, size/1000, "m184 187c-140 205-134 432-1 622l-66 44c-159-221-151-499 0-708z");
// "."
svgAddCircle(c, size/1000, 315, 675, 70, NO);
// "*"
svgAddPath(c, size/1000, "m652 277 107-35 21 63-109 36 68 92-54 39-68-93-66 91-52-41 67-88-109-37 21-63 108 37v-113h66v112z");
// ")"
svgAddPath(c, size/1000, "m816 813c140-205 134-430 1-621l66-45c159 221 151 499 0 708z");
CGContextSetFillColorWithColor(c, NSColor.whiteColor.CGColor);
CGContextFillPath(c);
}
#pragma mark - NSImage Name Registration #pragma mark - NSImage Name Registration
@@ -297,4 +315,5 @@ void RegisterImageViewNames(void) {
Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, YES); return YES; }); Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, YES); return YES; });
Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, NO); return YES; }); Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, NO); return YES; });
Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; }); Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; });
Register(32, RSSImageRegexIcon, NSLocalizedString(@"Regex icon", nil), ^(NSRect r) { DrawRegexIcon(r); return YES; });
} }

6
baRSS/Helper/TinySVG.h Normal file
View File

@@ -0,0 +1,6 @@
@import Cocoa;
CGMutablePathRef tinySVG_path(CGFloat scale, const char * code);
void svgAddPath(CGContextRef context, CGFloat scale, const char * path);
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise);
void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius);

174
baRSS/Helper/TinySVG.m Normal file
View File

@@ -0,0 +1,174 @@
#include "TinySVG.h"
struct SVGState {
CGFloat scale; // technically not part of parser but easier to pass along
char op;
float x, y;
bool prevDot;
float num[6];
uint8 iNum;
char buf[15];
uint8 iBuf;
};
# pragma mark - Helper
/// if number buffer contains anything, write it to num array and start new buffer
static void finishNum(struct SVGState *state) {
if (state->iBuf > 0) {
state->buf[state->iBuf] = '\0';
state->num[state->iNum++] = (float)atof(state->buf);
state->iBuf = 0;
state->prevDot = false;
}
}
/// All numbers stored in num array, finalize SVG path operation and add path to @c CGContext
static void finishOp(CGMutablePathRef path, struct SVGState *state) {
char op = state->op;
if (op >= 'a' && op <= 'z') {
// convert relative to absolute coordinates
for (uint8 t = 0; t < state->iNum; t++) {
state->num[t] += (t % 2 || op == 'v') ? state->y : state->x;
}
// convert to upper-case
op = op - 'a' + 'A';
}
if (op == 'Z') {
CGPathCloseSubpath(path);
} else if (op == 'V' && state->iNum == 1) {
state->y = state->num[0];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'H' && state->iNum == 1) {
state->x = state->num[0];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'M' && state->iNum == 2) {
state->x = state->num[0];
state->y = state->num[1];
CGPathMoveToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
// Edge-case: "M 1 2 3 4 5 6" is valid SVG after move 1,2 all remaining points are lines (3,4 and 5,6)
// For this case we overwrite op here. It will be overwritten again if a new op starts. Else, assume line-op.
state->op = (state->op == 'm') ? 'l' : 'L';
} else if (op == 'L' && state->iNum == 2) {
state->x = state->num[0];
state->y = state->num[1];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'C' && state->iNum == 6) {
state->x = state->num[4];
state->y = state->num[5];
CGPathAddCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, state->num[2] * state->scale, state->num[3] * state->scale, state->x * state->scale, state->y * state->scale);
} else {
NSLog(@"Unsupported SVG operation %c %d", state->op, state->iNum);
}
state->iNum = 0;
}
/// current number not finished yet. Append another char to internal buffer
inline static void continueNum(char chr, struct SVGState *state) {
state->buf[state->iBuf++] = chr;
}
# pragma mark - Parser
/// very basic svg path parser.
/// @returns @c CGMutablePathRef which must be released with @c CGPathRelease()
CGMutablePathRef tinySVG_path(CGFloat scale, const char * code) {
CGMutablePathRef path = CGPathCreateMutable();
struct SVGState state = {
.scale = scale,
.op = '_',
.x = 0,
.y = 0,
.prevDot = false,
//.num = {0, 0, 0, 0, 0, 0},
.iNum = 0,
//.buf = " ",
.iBuf = 0,
};
unsigned long len = strlen(code);
for (unsigned long i = 0; i < len; i++) {
char chr = code[i];
if ((chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')) {
if (state.op != '_') {
finishNum(&state);
finishOp(path, &state);
}
state.op = chr;
} else if (chr >= '0' && chr <= '9') {
continueNum(chr, &state);
} else if (chr == '-' && state.iBuf == 0) {
continueNum(chr, &state);
} else if (chr == '.' && !state.prevDot) {
continueNum(chr, &state);
state.prevDot = true;
} else { // any number-separating character
finishNum(&state);
// Edge-Case: SVG can reuse the previous operation without declaration
// e.g. you can draw four lines with "L1 2 3 4 5 6 7 8"
// or two curves with "c1 2 3 4 5 6 -1 -2 -3 -4 -5 -6"
// Therefore we need to complete the operation if the number of arguments is reached
if (state.iNum == 1 && strchr("HhVv", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 2 && strchr("MmLl", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 6 && strchr("Cc", state.op) != NULL) {
finishOp(path, &state);
}
if (chr == '-') {
continueNum(chr, &state);
} else if (chr == '.') {
continueNum(chr, &state);
state.prevDot = true;
}
}
}
return path;
}
# pragma mark - External API
/// calls @c tinySVG_path and handles @c CGPath creation and release.
void svgAddPath(CGContextRef context, CGFloat scale, const char * path) {
CGMutablePathRef tmp = tinySVG_path(scale, path);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
}
/// calls @c CGPathAddArc with full circle
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) {
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddArc(tmp, NULL, x * scale, y * scale, radius * scale, 0, M_PI * 2, clockwise);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
}
/// Calls @c CGContextAddRect or @c CGPathAddRoundedRect (optional).
/// @param cornerRadius Use @c <=0 for no corners. Use half of @c min(w,h) for a full circle.
void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius) {
if (cornerRadius > 0) {
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddRoundedRect(tmp, NULL, rect, cornerRadius * scale, cornerRadius * scale);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
} else {
CGContextAddRect(context, rect);
}
}