Add custom sliders

This commit is contained in:
Mikhail Lobotskiy
2025-07-02 23:19:23 +04:00
parent b9c09c6b8d
commit f3d60dbbad
12 changed files with 262 additions and 52 deletions

View File

@ -49,8 +49,9 @@ void CCefViewMedia::showMediaControl(NSEditorApi::CAscExternalMediaPlayerCommand
}
m_player_view->SetMedia(data->get_Url());
// TODO: remove play
// TODO: remove play and mute
m_player_view->Play();
m_player_view->ChangeVolume(0);
int nVolume = data->get_Volume();
if (data->get_Mute() || m_pCefView->IsPresentationReporter()) {

View File

@ -2,6 +2,7 @@
#import "iconpushbutton.h"
#import "timelabel.h"
#import "slider.h"
#import "utils.h"
// Helper functions for maintaining auto layout
@ -45,6 +46,8 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
NSIconPushButton* m_btn_rewind_back;
// text labels
NSTimeLabel* m_time_label;
// sliders
NSStyledSlider* m_slider_video;
// constraints
NSLayoutConstraint* m_time_label_width_constraint;
NSLayoutConstraint* m_time_label_height_constraint;
@ -72,7 +75,7 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
const NSSize button_size = NSMakeSize(button_width, button_width);
// play button
m_btn_play = [[NSIconPushButton alloc] initWithIconName:@"btn-play" size:button_size skin:&m_skin];
m_btn_play = [[NSIconPushButton alloc] initWithIconName:@"btn-play" size:button_size style:&m_skin.button];
[self addSubview:m_btn_play];
setLeftConstraintsToView(m_btn_play, self.topAnchor, self.leftAnchor, button_y_offset, button_space_between, button_size);
#if !__has_feature(objc_arc)
@ -80,7 +83,7 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
#endif
// volume button
m_btn_volume = [[NSIconPushButton alloc] initWithIconName:@"btn-volume-2" size:button_size skin:&m_skin];
m_btn_volume = [[NSIconPushButton alloc] initWithIconName:@"btn-volume-2" size:button_size style:&m_skin.button];
[self addSubview:m_btn_volume];
setRightConstraintsToView(m_btn_volume, self.topAnchor, self.rightAnchor, button_y_offset, button_space_between, button_size);
#if !__has_feature(objc_arc)
@ -88,14 +91,14 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
#endif
// time label
m_time_label = [[NSTimeLabel alloc] initWithSkin:&m_skin];
m_time_label = [[NSTimeLabel alloc] initWithStyle:&m_skin.time_label];
[self addSubview:m_time_label];
NSSize text_bounds = [m_time_label getBoundingBoxSize];
// manually set all constraints
// manually set all text label constraints
m_time_label.translatesAutoresizingMaskIntoConstraints = NO;
[m_time_label.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:0].active = YES;
[m_time_label.rightAnchor constraintEqualToAnchor:m_btn_volume.leftAnchor constant:-button_space_between].active = YES;
// we need to save width and height constraints becuase changing skin may affect text width and height
// we need to save width and height constraints because changing skin may affect text width and height
m_time_label_width_constraint = [m_time_label.widthAnchor constraintEqualToConstant:text_bounds.width];
m_time_label_width_constraint.active = YES;
m_time_label_height_constraint = [m_time_label.heightAnchor constraintEqualToConstant:text_bounds.height];
@ -105,7 +108,7 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
#endif
// rewind forward button
m_btn_rewind_forward = [[NSIconPushButton alloc] initWithIconName:@"btn-rewind-forward" size:button_size skin:&m_skin];
m_btn_rewind_forward = [[NSIconPushButton alloc] initWithIconName:@"btn-rewind-forward" size:button_size style:&m_skin.button];
[self addSubview:m_btn_rewind_forward];
setRightConstraintsToView(m_btn_rewind_forward, self.topAnchor, m_time_label.leftAnchor, button_y_offset, button_space_between, button_size);
#if !__has_feature(objc_arc)
@ -113,7 +116,7 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
#endif
// rewind back button
m_btn_rewind_back = [[NSIconPushButton alloc] initWithIconName:@"btn-rewind-back" size:button_size skin:&m_skin];
m_btn_rewind_back = [[NSIconPushButton alloc] initWithIconName:@"btn-rewind-back" size:button_size style:&m_skin.button];
[self addSubview:m_btn_rewind_back];
setRightConstraintsToView(m_btn_rewind_back, self.topAnchor, m_btn_rewind_forward.leftAnchor, button_y_offset, button_space_between, button_size);
#if !__has_feature(objc_arc)
@ -121,7 +124,18 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
#endif
// video slider
// TODO:
m_slider_video = [[NSStyledSlider alloc] initWithStyle:&m_skin.video_slider];
[self addSubview:m_slider_video];
// manually set all constraints
m_slider_video.translatesAutoresizingMaskIntoConstraints = NO;
[m_slider_video.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:0].active = YES;
[m_slider_video.leftAnchor constraintEqualToAnchor:m_btn_play.rightAnchor constant:button_space_between].active = YES;
[m_slider_video.rightAnchor constraintEqualToAnchor:m_btn_rewind_back.leftAnchor constant:-button_space_between].active = YES;
// TODO: the whole slider area on panel becomes clickable because of this
[m_slider_video.heightAnchor constraintEqualToConstant:frame_rect.size.height].active = YES;
#if !__has_feature(objc_arc)
[m_slider_video release];
#endif
}
return self;
}
@ -154,6 +168,8 @@ void setRightConstraintsToView(NSView* view, NSLayoutYAxisAnchor* top_anchor, NS
NSSize text_bounds = [m_time_label getBoundingBoxSize];
m_time_label_width_constraint.constant = text_bounds.width;
m_time_label_height_constraint.constant = text_bounds.height;
// update sliders
[m_slider_video updateStyle];
}
@end

View File

@ -24,6 +24,27 @@ struct CTimeLabelStyle {
Color color;
};
struct CSliderStyle {
struct CTrackStyle {
Color color; // color of the track (right of knob)
Color fill_color; // color of the filled portion of the track (left of knob)
int thickness;
int border_radius;
};
struct CKnobStyle {
Color color;
Color border_color;
int thickness;
int border_width = 0;
int border_radius = 0;
};
CTrackStyle track;
bool is_knob_visible;
CKnobStyle knob;
};
class CFooterSkin {
public:
enum class Type {
@ -49,19 +70,21 @@ public:
CFooterStyle footer;
CButtonStyle button;
CTimeLabelStyle time_label;
CSliderStyle video_slider;
CSliderStyle volume_slider;
public:
// some global constants (skin-independent)
// buttons
static constexpr int button_width = 30;
static constexpr int button_y_offset = 5;
static constexpr int button_space_between = 8;
// volume controls
static constexpr int volume_control_width = 30;
static constexpr int volume_control_height = 140;
static constexpr int volume_slider_width = 20;
static constexpr int volume_slider_height = 120;
// general footer panel params
static constexpr int border_radius = 5;
};

View File

@ -13,15 +13,22 @@ CFooterSkin CFooterSkin::getSkin(Type type) {
skin.button.bg_color_pressed = 0xCBCBCB;
skin.button.border_radius = 3;
// skin.m_oSliderStyleOpt1.m_sAddColor = "#848484";
// skin.m_oSliderStyleOpt1.m_sSubColor = "#E2E2E2";
// skin.m_oSliderStyleOpt1.m_sHandleColor = "#FFFFFF";
// skin.m_oSliderStyleOpt1.m_sHandleBorderColor = "#444444";
skin.video_slider.track.color = 0xE2E2E2;
skin.video_slider.track.fill_color = 0x848484;
skin.video_slider.track.thickness = 8;
skin.video_slider.track.border_radius = 4;
skin.video_slider.is_knob_visible = false;
// skin.m_oSliderStyleOpt2.m_sAddColor = "#C0C0C0";
// skin.m_oSliderStyleOpt2.m_sSubColor = "#E2E2E2";
// skin.m_oSliderStyleOpt2.m_sHandleColor = "#FFFFFF";
// skin.m_oSliderStyleOpt2.m_sHandleBorderColor = "#444444";
skin.volume_slider.track.color = 0xE2E2E2;
skin.volume_slider.track.fill_color = 0xC0C0C0;
skin.volume_slider.track.thickness = 8;
skin.volume_slider.track.border_radius = 4;
skin.volume_slider.is_knob_visible = true;
skin.volume_slider.knob.color = 0xFFFFFF;
skin.volume_slider.knob.border_color = 0x444444;
skin.volume_slider.knob.thickness = 16;
skin.volume_slider.knob.border_width = 2;
skin.volume_slider.knob.border_radius = 8;
skin.time_label.font_name = @"";
skin.time_label.font_size = 14;
@ -37,15 +44,22 @@ CFooterSkin CFooterSkin::getSkin(Type type) {
skin.button.bg_color_pressed = 0x46494B;
skin.button.border_radius = 3;
// skin.m_oSliderStyleOpt1.m_sAddColor = "#9B9B9B";
// skin.m_oSliderStyleOpt1.m_sSubColor = "#545454";
// skin.m_oSliderStyleOpt1.m_sHandleColor = "#FFFFFF";
// skin.m_oSliderStyleOpt1.m_sHandleBorderColor = "#222222";
skin.video_slider.track.color = 0x545454;
skin.video_slider.track.fill_color = 0x9B9B9B;
skin.video_slider.track.thickness = 8;
skin.video_slider.track.border_radius = 4;
skin.video_slider.is_knob_visible = false;
// skin.m_oSliderStyleOpt2.m_sAddColor = "#808080";
// skin.m_oSliderStyleOpt2.m_sSubColor = "#545454";
// skin.m_oSliderStyleOpt2.m_sHandleColor = "#FFFFFF";
// skin.m_oSliderStyleOpt2.m_sHandleBorderColor = "#222222";
skin.volume_slider.track.color = 0x545454;
skin.volume_slider.track.fill_color = 0x808080;
skin.volume_slider.track.thickness = 8;
skin.volume_slider.track.border_radius = 4;
skin.volume_slider.is_knob_visible = true;
skin.volume_slider.knob.color = 0xFFFFFF;
skin.volume_slider.knob.border_color = 0x222222;
skin.volume_slider.knob.thickness = 16;
skin.volume_slider.knob.border_width = 2;
skin.volume_slider.knob.border_radius = 8;
skin.time_label.font_name = @"";
skin.time_label.font_size = 14;

View File

@ -6,7 +6,7 @@
#import "footerskin.h"
@interface NSIconPushButton : NSButton
- (instancetype)initWithIconName:(NSString*)icon_name size:(NSSize)size skin:(CFooterSkin*)skin;
- (instancetype)initWithIconName:(NSString*)icon_name size:(NSSize)size style:(CButtonStyle*)style;
- (void)dealloc;
// button appearance

View File

@ -4,7 +4,7 @@
@interface NSIconPushButton ()
{
CFooterSkin* m_skin;
CButtonStyle* m_style;
bool m_hovered;
NSString* m_icon_name;
}
@ -12,13 +12,13 @@
@implementation NSIconPushButton
- (instancetype)initWithIconName:(NSString*)icon_name size:(NSSize)size skin:(CFooterSkin*)skin {
- (instancetype)initWithIconName:(NSString*)icon_name size:(NSSize)size style:(CButtonStyle*)style {
NSRect init_frame_rect = NSMakeRect(0, 0, size.width, size.height);
self = [super initWithFrame:init_frame_rect];
if (self) {
[self setWantsLayer:YES];
// apply skin
m_skin = skin;
m_style = style;
m_icon_name = icon_name;
[self updateStyle];
// add mouse tracking for background color or/and icon changing
@ -35,16 +35,16 @@
}
- (void)updateStyle {
self.layer.cornerRadius = m_skin->button.border_radius;
self.layer.cornerRadius = m_style->border_radius;
self.bordered = NO;
self.layer.backgroundColor = [NSColorFromHex(m_skin->button.bg_color_regular) CGColor];
self.layer.backgroundColor = [NSColorFromHex(m_style->bg_color_regular) CGColor];
[self setIcon:m_icon_name];
}
- (void)setIcon:(NSString*)icon_name {
m_icon_name = icon_name;
// TODO: are -2x png icons always suitable on mac platforms ???
NSString* icon_full_name = [NSString stringWithFormat:@"%@%@%@", icon_name, m_skin->button.icon_postfix, @"-2x"];
NSString* icon_full_name = [NSString stringWithFormat:@"%@%@%@", icon_name, m_style->icon_postfix, @"-2x"];
NSString* icon_path = [[NSBundle mainBundle] pathForResource:icon_full_name ofType:@"png"];
if (icon_path == nil) {
NSLog(@"Error: could not load icon %@.png", icon_full_name);
@ -60,25 +60,25 @@
- (void)mouseEntered:(NSEvent*)event {
m_hovered = true;
self.layer.backgroundColor = [NSColorFromHex(m_skin->button.bg_color_hovered) CGColor];
self.layer.backgroundColor = [NSColorFromHex(m_style->bg_color_hovered) CGColor];
}
- (void)mouseExited:(NSEvent*)event {
m_hovered = false;
self.layer.backgroundColor = [NSColorFromHex(m_skin->button.bg_color_regular) CGColor];
self.layer.backgroundColor = [NSColorFromHex(m_style->bg_color_regular) CGColor];
}
- (void)mouseDown:(NSEvent*)event {
self.layer.backgroundColor = [NSColorFromHex(m_skin->button.bg_color_pressed) CGColor];
self.layer.backgroundColor = [NSColorFromHex(m_style->bg_color_pressed) CGColor];
NSLog(@"debug: button pressed");
}
- (void)mouseUp:(NSEvent*)event {
CGColor* bg_color = nil;
if (m_hovered) {
bg_color = [NSColorFromHex(m_skin->button.bg_color_hovered) CGColor];
bg_color = [NSColorFromHex(m_style->bg_color_hovered) CGColor];
} else {
bg_color = [NSColorFromHex(m_skin->button.bg_color_regular) CGColor];
bg_color = [NSColorFromHex(m_style->bg_color_regular) CGColor];
}
self.layer.backgroundColor = bg_color;
NSLog(@"debug: button released");

View File

@ -32,7 +32,7 @@ public:
void Play();
void Pause();
void TogglePause();
void ChangeVolume(int new_value);
void ChangeVolume(CGFloat new_value);
void ToggleMute();
bool SetMedia(const std::wstring& media_path);
void Stop();

View File

@ -59,8 +59,8 @@ void CPlayerView::TogglePause() {
}
}
void CPlayerView::ChangeVolume(int new_value) {
// TODO
void CPlayerView::ChangeVolume(CGFloat new_value) {
m_player.volume = new_value;
}
void CPlayerView::ToggleMute() {

View File

@ -3,8 +3,17 @@
#import <Cocoa/Cocoa.h>
@interface NSVideoSlider : NSSlider
#import "footerskin.h"
@interface NSStyledSlider : NSSlider
- (instancetype)initWithStyle:(CSliderStyle*)style;
- (void)dealloc;
// slider appearance
- (void)updateStyle;
// test action function
- (void)sliderValueChanged:(NSSlider*)sender;
@end
#endif // VIDEOPLAYER_SLIDER_H_

View File

@ -0,0 +1,147 @@
#import "slider.h"
#import "utils.h"
@interface NSStyledSliderCell : NSSliderCell
{
CSliderStyle* m_style;
NSImage* m_knob_image;
NSRect m_track_rect;
}
- (instancetype)initWithStyle:(CSliderStyle*)style;
- (void)updateStyle;
@end
@implementation NSStyledSliderCell
- (instancetype)initWithStyle:(CSliderStyle*)style {
self = [super init];
if (self) {
// set skin
m_style = style;
[self updateStyle];
}
return self;
}
- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {
// draw the whole track
const CGFloat track_thickness = m_style->track.thickness;
const CGFloat track_border_radius = m_style->track.border_radius;
m_track_rect = NSInsetRect(rect, 0, (rect.size.height - track_thickness) / 2);
NSBezierPath* track_path = [NSBezierPath bezierPathWithRoundedRect:m_track_rect xRadius:track_border_radius yRadius:track_border_radius];
[NSColorFromHex(m_style->track.color) setFill];
[track_path fill];
// draw the filled portion of track (left of knob)
double value = (self.doubleValue - self.minValue) / (self.maxValue - self.minValue);
NSRect filled_rect = m_track_rect;
filled_rect.size.width *= value;
[NSColorFromHex(m_style->track.fill_color) setFill];
// TODO: when knob is visible, the right part of the filled rect does not have to be rounded
NSBezierPath* filled_path = [NSBezierPath bezierPathWithRoundedRect:filled_rect xRadius:track_border_radius yRadius:track_border_radius];
[filled_path fill];
}
- (NSRect)knobRectFlipped:(BOOL)flipped {
// calculate knob center offset relative to slider current value
// by default we stick knob edge to the edge of the slider track
CGFloat knob_center_max_offset = m_knob_image.size.width / 2;
if (knob_center_max_offset >= m_track_rect.origin.x) {
// if the knob is big enough, then stick it to the control rect edge
knob_center_max_offset -= m_track_rect.origin.x;
}
// this formula essentially means the following:
// - near the edges the knob will have corresponding offset from slider exact value, to be able to fit into control rect
// - at the middle the offset is zero, meaning the knob positioned exactly where slider is
// - the offset changes lineary and flips its sign when knob passes the middle point
CGFloat knob_center_offset = -(knob_center_max_offset * self.doubleValue) / ((self.maxValue - self.minValue) / 2) + knob_center_max_offset;
// get current slider value
double value = (self.doubleValue - self.minValue) / (self.maxValue - self.minValue);
CGFloat slider_pos = m_track_rect.size.width * value + m_track_rect.origin.x;
// create knob rect from the center point
CGFloat knob_center_pos = slider_pos + knob_center_offset;
NSRect knob_rect = NSMakeRect(knob_center_pos - m_knob_image.size.width / 2, NSMidY(m_track_rect) - m_knob_image.size.height / 2, m_knob_image.size.width, m_knob_image.size.height);
return knob_rect;
}
- (void)drawKnob:(NSRect)knob_rect {
if (m_style->is_knob_visible) {
[m_knob_image drawInRect:knob_rect];
}
}
- (void)updateStyle {
// set knob image
if (m_style->is_knob_visible) {
const CGFloat knob_thickness = m_style->knob.thickness;
m_knob_image = [[NSImage imageWithSize:NSMakeSize(knob_thickness, knob_thickness) flipped:NO drawingHandler:^BOOL(NSRect dst_rect) {
// TODO: add border
const CGFloat border_radius = m_style->knob.border_radius;
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:dst_rect xRadius:border_radius yRadius:border_radius];
[NSColorFromHex(m_style->knob.color) setFill];
[path fill];
return YES;
}] retain];
} else {
const CGFloat knob_thickness = m_style->track.thickness;
m_knob_image = [[NSImage alloc] initWithSize:NSMakeSize(knob_thickness, knob_thickness)];
}
}
- (void)dealloc {
NSLog(@"debug: slider cell deallocated");
#if !__has_feature(objc_arc)
[m_knob_image release];
[super dealloc];
#endif
}
@end
@interface NSStyledSlider ()
{
NSStyledSliderCell* m_cell;
}
@end
@implementation NSStyledSlider
- (void)setNeedsDisplayInRect:(NSRect)invalid_rect {
[super setNeedsDisplayInRect:[self bounds]];
}
- (instancetype)initWithStyle:(CSliderStyle*)style {
self = [super init];
if (self) {
[self setWantsLayer:YES];
// set cell
m_cell = [[NSStyledSliderCell alloc] initWithStyle:style];
[self setCell:m_cell];
// set min and max values
self.minValue = 0.0;
self.maxValue = 100.0;
// set action
[self setTarget:self];
[self setAction:@selector(sliderValueChanged:)];
}
return self;
}
- (void)dealloc {
NSLog(@"debug: slider deallocated");
#if !__has_feature(objc_arc)
[m_cell release];
[super dealloc];
#endif
}
- (void)updateStyle {
[m_cell updateStyle];
}
- (void)sliderValueChanged:(NSSlider*)sender {
NSLog(@"debug: slider value changed: %.2f", sender.doubleValue);
}
@end

View File

@ -6,7 +6,7 @@
#import "footerskin.h"
@interface NSTimeLabel : NSTextField
- (instancetype)initWithSkin:(CFooterSkin*)skin;
- (instancetype)initWithStyle:(CTimeLabelStyle*)style;
- (void)dealloc;
// time label appearance and content

View File

@ -4,7 +4,7 @@
@interface NSTimeLabel ()
{
CFooterSkin* m_skin;
CTimeLabelStyle* m_style;
NSDictionary* m_attributes;
NSSize m_bounding_box_size;
}
@ -12,12 +12,12 @@
@implementation NSTimeLabel
- (instancetype)initWithSkin:(CFooterSkin*)skin {
- (instancetype)initWithStyle:(CTimeLabelStyle*)style {
self = [super init];
if (self) {
[self setWantsLayer:YES];
// apply skin
m_skin = skin;
m_style = style;
[self updateStyle];
// set initial time
[self setText:@"00:00:00"];
@ -40,18 +40,18 @@
- (void)updateStyle {
// get font
CGFloat font_size = m_skin->time_label.font_size;
CGFloat font_size = m_style->font_size;
NSFont* font = nil;
if (m_skin->time_label.font_name.length == 0) {
if (m_style->font_name.length == 0) {
font = [NSFont systemFontOfSize:font_size];
} else {
font = [NSFont fontWithName:m_skin->time_label.font_name size:font_size];
font = [NSFont fontWithName:m_style->font_name size:font_size];
if (!font) {
font = [NSFont systemFontOfSize:font_size];
}
}
// get color
NSColor* color = NSColorFromHex(m_skin->time_label.color);
NSColor* color = NSColorFromHex(m_style->color);
// update attributes
m_attributes = @{
NSFontAttributeName: font,