diff options
-rw-r--r-- | doc/image.n | 14 | ||||
-rw-r--r-- | doc/nsimage.n | 129 | ||||
-rw-r--r-- | doc/ttk_button.n | 7 | ||||
-rw-r--r-- | library/demos/images/Tk_feather.png | bin | 0 -> 119158 bytes | |||
-rw-r--r-- | library/demos/images/plowed_field.png | bin | 0 -> 8363308 bytes | |||
-rw-r--r-- | library/demos/images/starry_night.png | bin | 0 -> 8015314 bytes | |||
-rw-r--r-- | library/demos/mac_styles.tcl | 268 | ||||
-rw-r--r-- | library/demos/widget | 9 | ||||
-rw-r--r-- | library/ttk/aquaTheme.tcl | 57 | ||||
-rw-r--r-- | library/ttk/combobox.tcl | 233 | ||||
-rw-r--r-- | library/ttk/menubutton.tcl | 10 | ||||
-rw-r--r-- | library/ttk/progress.tcl | 6 | ||||
-rw-r--r-- | macosx/tkMacOSXConstants.h | 3 | ||||
-rw-r--r-- | macosx/tkMacOSXDraw.c | 2 | ||||
-rw-r--r-- | macosx/tkMacOSXImage.c | 716 | ||||
-rw-r--r-- | macosx/tkMacOSXInit.c | 121 | ||||
-rw-r--r-- | macosx/tkMacOSXMenu.c | 1 | ||||
-rw-r--r-- | macosx/tkMacOSXPrivate.h | 4 | ||||
-rw-r--r-- | macosx/tkMacOSXRegion.c | 12 | ||||
-rw-r--r-- | macosx/tkMacOSXSubwindows.c | 2 | ||||
-rw-r--r-- | macosx/ttkMacOSXTheme.c | 2962 | ||||
-rw-r--r-- | macosx/ttkMacOSXTheme.h | 563 | ||||
-rw-r--r-- | tests/ttk/combobox.test | 10 |
23 files changed, 3674 insertions, 1455 deletions
diff --git a/doc/image.n b/doc/image.n index b620076..24b768a 100644 --- a/doc/image.n +++ b/doc/image.n @@ -93,8 +93,20 @@ See the \fBbitmap\fR manual entry for more information. Displays a variety of full-color images, using dithering to approximate colors on displays with limited color capabilities. See the \fBphoto\fR manual entry for more information. +.TP +\fBnsimage\fR +This type is only available in the Aqua platform. It is a full-color +image which may be created from a named system image. It has options +designed to facilitate the use of these images in buttons. An +\fBnsimage\fR can be based on an a \fItemplate image\fR, which is an +anti-aliased 2-color image format that automatically interchanges +black and white in response to changes between the light and dark +modes on macOS. An \fBnsimage\fR can also generate an alternate +\fIpressed\fR variant which is lighter in dark mode and darker in +light mode. These are for use in image buttons. See the \fBnsimage\fR +manual entry for more information. .SH "SEE ALSO" -bitmap(n), options(n), photo(n) +bitmap(n), options(n), photo(n), nsimage(n) .SH KEYWORDS height, image, types of images, width '\" Local Variables: diff --git a/doc/nsimage.n b/doc/nsimage.n new file mode 100644 index 0000000..8b401d7 --- /dev/null +++ b/doc/nsimage.n @@ -0,0 +1,129 @@ +'\" +'\" Copyright (c) 2022 Marc Culler +'\" +'\" See the file "license.terms" for information on usage and redistribution +'\" of this file, and for a DISCLAIMER OF ALL WARRANTIES. +'\" +.TH nsimage n 8.7 Tk "Tk Built-In Commands" +.so man.macros +.BS +'\" Note: do not modify the .SH NAME line immediately below! +.SH NAME +nsimage \- A Tk image type for macOS based on the NSImage class. +.BE +.SH DESCRIPTION +The nsimage is implemented as a Tk image type. The \fBimage\fR +command is used to create, delete, and query all images, including +images of type \fBnsimage\fR. The options that are available are +specific to the nsimage type and are described below. +.PP +The command to create an \fBnsimage\fR: +.TP +\fBimage create nsimage \fR?\fIname\fR? ?\fIoption value ...\fR? +.PP +creates a new nsimage and a command with the same name and returns its +name. +.SH OPTIONS +.PP +Valid \fIoptions\fR are: +.TP +\fB\-source\fR +.PP +The value of the \fB\-source\fR option is a string describing an +NSimage. There are several ways to interpret this string, and the +interpretation is determined by the value of the \fB\-as\fR option. +This option is required. +.PP +.TP +\fB\-as\fR +.PP +There are four possible values for the \fB-as\fR option which specify +how the source string should be interpreted. The allowed values and +their meanings are: +.IP +\fBname\fR +.IP +The source should be interpreted as the name of a named NSImage +provided by the system. This is the default if the -as option is not specified. +.IP +\fBfile\fR +.IP +The source should be interpreted as a path to an image +file in one of the formats understood by the NSImage class. +.IP +\fBpath\fR +.IP +The source should be interpreted as a path to an arbitrary +file. The type of the file will be examined and the resulting image +will be the system icon for files of that type. +.IP +\fBfiletype\fR +.IP +The source is interpreted as a string identifying a +particular file type. It may be a filename extension, an Apple Uniform Type +Identifier or a 4-character OSType value as used in the HFS filesystem. +.TP +\fB\-width\fR +.PP +The value of the \fIwidth\fR option is an integer specifying the width +in pixels of the nsimage. If the width is not specified it will be +computed from the height so as to preserve the aspect ration. If +neither width nor height are specified then the width and height of +the underlying NSImage will be used. +.TP +\fB\-height\fR +.PP +The value of the \fIheight\fR option is an integer specifying the +height in pixels of the nsimage. If the height is not specified it +will be computed from the height so as to preserve the aspect ration. +If neither width nor height are specified then the width and height of +the underlying NSImage will be used. +.TP +\fB\-radius\fR +.PP +The value of the \fIradius\fR option is an integer. If non-zero the +image will be clipped to a rounded rectangle with the same width and +height as the image, but with circular arcs of the specified radius +cutting off the corners of the rectangle. +.TP +\fB\-ring\fR +.PP +The value of the \fIring\fR option is an integer. If non-zero then it +specifies the thickness of a focus ring which will be drawn around the +image using the control accent color specified in the System +Preferences. The image is resized to reduce its width and height by +twice the thickness of the ring. Note that this may create a small +amount of distortion. The aspect ration of a non-square image will +change slightly. +.TP +\fB\-alpha\fR +.PP +The value of the \fIalpha\fR option should be a floating point number +between 0.0 and 1.0. This alpha value will be applied to each pixel +of the nsimage, producing a partially transparent image. The default value +is 1,0, which makes the image opaque. +.TP +\fB\-pressed\fR +.PP +The \fIpressed\fR option takes a boolean value. If the value is true +or 1 then the image will be algorithmically modified to become darker +in light mode or lighter in dark mode. The default is false. For an +image button, the primary image should use the value false while the +pressed image should be the same image but with the \fIpressed\fR +option set to true. +.TP +\fB\-template\fR +.PP +The \fItemplate\fR option takes a boolean value. If the value is true +or 1 then the image will be marked as being a template image. This +means that the system will algorithmically convert the image to a +light colored image when in dark mode. For the algorithm to work +correctly the image must consist only of black pixels with alpha +values. +.SH "SEE ALSO" +image(n), options(n), photo(n) +.SH KEYWORDS +height, image, types of images, width +'\" Local Variables: +'\" mode: nroff +'\" End: diff --git a/doc/ttk_button.n b/doc/ttk_button.n index 7afd052..896432f 100644 --- a/doc/ttk_button.n +++ b/doc/ttk_button.n @@ -59,6 +59,13 @@ Invokes the command associated with the button. .PP \fBTtk::button\fR widgets support the \fBToolbutton\fR style in all standard themes, which is useful for creating widgets for toolbars. +.PP +In the Aqua theme there are several other styles which can be used to +produce replicas of many of the different button types that are +discussed in Apple's Human Interface Guidelines. These include +\fBDisclosureButton\fR, \fBDisclosureTriangle\fR, \fBHelpButton\fR, +\fBImageButton\fR, \fBInlineButton\fR, \fBGradientButton\fR, +\fBRoundedRectButton\fR, and \fBRecessedButton\fR. .SH "STYLING OPTIONS" .PP The class name for a \fBttk::button\fP is \fBTButton\fP. diff --git a/library/demos/images/Tk_feather.png b/library/demos/images/Tk_feather.png Binary files differnew file mode 100644 index 0000000..226042b --- /dev/null +++ b/library/demos/images/Tk_feather.png diff --git a/library/demos/images/plowed_field.png b/library/demos/images/plowed_field.png Binary files differnew file mode 100644 index 0000000..2103b6b --- /dev/null +++ b/library/demos/images/plowed_field.png diff --git a/library/demos/images/starry_night.png b/library/demos/images/starry_night.png Binary files differnew file mode 100644 index 0000000..f65e408 --- /dev/null +++ b/library/demos/images/starry_night.png diff --git a/library/demos/mac_styles.tcl b/library/demos/mac_styles.tcl new file mode 100644 index 0000000..df727df --- /dev/null +++ b/library/demos/mac_styles.tcl @@ -0,0 +1,268 @@ +# mac_styles.tcl -- +# +# This demonstration script creates a toplevel window containing a notebook +# whose pages provide examples of the various mac-specific widgets that are +# provided via special values for the -style option. + +if {![info exists widgetDemo]} { + error "This script should be run from the \"widget\" demo." +} + +package require Tk + +set w .mac_styles +catch {destroy $w} +toplevel $w +package require Tk +wm title $w "Tk Aqua Widgets" +wm iconname $w "mac_styles" +positionWindow $w +## +# System images we use in our buttons + +set featherImg [file join $tk_demoDirectory images Tk_feather.png] +set starryImg [file join $tk_demoDirectory images starry_night.png] +set fieldImg [file join $tk_demoDirectory images plowed_field.png] +image create nsimage action -source NSAction -width 48 -height 48 +image create nsimage bonjour -source NSBonjour -width 48 -height 48 +image create nsimage bonjour1 -source NSBonjour -width 48 -height 48 -pressed 1 +image create nsimage tkfeather -source $featherImg -as file -width 48 -height 48 +image create nsimage tkfeather1 -source $featherImg -as file -width 48 -height 48 -pressed 1 +image create nsimage starry -source $starryImg -as file -width 96 -radius 10 +image create nsimage starry1 -source $starryImg -as file -width 96 -radius 10 -pressed 1 +image create nsimage starry2 -source $starryImg -as file -width 96 -radius 10 -ring 3 +image create nsimage field -source $fieldImg -as file -width 96 -radius 10 +image create nsimage field1 -source $fieldImg -as file -width 96 -radius 10 -pressed 1 +image create nsimage field2 -source $fieldImg -as file -width 96 -radius 10 -ring 3 +image create nsimage add -source NSAddTemplate -width 11 -height 11 +image create nsimage remove -source NSRemoveTemplate -width 11 -height 11 + +# Off state and variables for checkbuttons and radio buttons +set off {!selected !alternate} +variable $w.checkVar +variable $w.radioVar +variable $w.triangleVar +variable $w.popupVar +variable $w.stepVar +variable $w.comboVar + +# Make a disclosure triangle change state when clicked. +proc toggleTriangle {tri} { + $tri instate {user1} { + $tri state {!user1} + return + } + $tri instate {!user1} { + $tri state {user1} + } +} + +proc popupButton {win varName firstValue args} { + upvar #0 $varName var + if {![info exists var]} { + set var $firstValue + } + ttk::menubutton $win -textvariable $varName -menu $win.menu -direction flush + menu $win.menu -tearoff 0 + $win.menu add radiobutton -label $firstValue -variable $varName + foreach i $args { + $win.menu add radiobutton -label $i -variable $varName + } + return $win.menu +} + +set mag [encoding convertfrom utf-8 "\xf0\x9f\x94\x8d"] + +proc searchFocusOut {e} { + global mag + if {[$e get] eq ""} { + $e configure -foreground gray60 + $e insert 0 "Search" + } +} + +proc searchFocusIn {e} { + if {[$e cget -foreground] == {gray60}} { + $e delete 0 end + $e configure -foreground black + } +} + +## Make the notebook and set up Ctrl+Tab traversal +ttk::notebook $w.notebook +ttk::notebook::enableTraversal $w.notebook + +# Frames pane +set framesFrame [ttk::frame $w.notebook.frames -padding {40 35 40 50}] +$w.notebook add $framesFrame -text "Frames" +pack [ttk::labelframe $framesFrame.darker -text Darker -padding {50 30 50 50}] \ + -fill both -expand 1 +pack [ttk::label $framesFrame.darker.label -padding {0 0 0 6} \ + -text "This Group Box is nested to depth 2"] \ + -fill x +pack [ttk::labelframe $framesFrame.darker.darker -text "Darker Still" -padding 24] \ + -fill both -expand 1 +pack [ttk::label $framesFrame.darker.darker.label \ + -text "This Group Box is nested to depth 3"] -fill x +pack [button $framesFrame.darker.darker.tkbutton -text "Tk Button" -width 7 \ + -highlightbackground systemWindowBackgroundColor3] -pady 10 +pack [ttk::button $framesFrame.darker.darker.ttkbutton -text "Ttk Button" \ + -width 7 -padding {-4 0 -4 0}] \ + -pady 3 +# Button pane +set buttonFrame [ttk::frame $w.notebook.buttons -padding {100 20 0 20}] +$w.notebook add $buttonFrame -text "Buttons" +grid columnconfigure $buttonFrame 0 -minsize 100 +grid columnconfigure $buttonFrame 1 -minsize 100 + +set plain [ttk::button $buttonFrame.plain -text Button -padding {-12 0}] +popupButton $buttonFrame.options .popupVar "Item 1" "Item 2" "Item 3" +set options $buttonFrame.options +set check [ttk::checkbutton $buttonFrame.check -text Check -variable .checkVar] +set radio [ttk::frame $buttonFrame.radio] +pack [ttk::radiobutton $radio.r1 -text "Radio 1" -variable .radioVar -value 1] -pady 4 +pack [ttk::radiobutton $radio.r2 -text "Radio 2" -variable .radioVar -value 2] -pady 4 +set triangle [ttk::checkbutton $buttonFrame.triangle -style Item -variable TriangleVar] +bind $triangle <Button-1> {toggleTriangle %W} +set bonjour [ttk::button $buttonFrame.bonjour -style ImageButton -text Bonjour \ + -image {bonjour pressed bonjour1}] +set feather [ttk::button $buttonFrame.feather -style ImageButton -text Tk \ + -image {tkfeather pressed tkfeather1}] +set gradient [ttk::frame $buttonFrame.gradient] +pack [ttk::button $buttonFrame.gradient.add -style GradientButton \ + -image add -padding 7] -side left +pack [ttk::button $buttonFrame.gradient.remove -style GradientButton \ + -image remove -padding 7] -side left +set disclosure [ttk::checkbutton $buttonFrame.disclosure -style DisclosureButton] +set help [ttk::button $buttonFrame.help -style HelpButton]; + +$check state $off +$radio.r1 state $off +$radio.r2 state $off + +grid [ttk::label $buttonFrame.plainLabel -text "Push Button:"]\ + -row 0 -column 0 -padx 4 -sticky e +grid $plain -pady 4 -row 0 -column 1 -sticky w +grid [ttk::label $buttonFrame.optionsLabel -text "Pop-up Button:"]\ + -row 1 -column 0 -padx 4 -sticky e +grid $options -pady 4 -row 1 -column 1 -sticky w +grid [ttk::label $buttonFrame.checkLabel -text "Check Button:"]\ + -row 2 -column 0 -padx 4 -sticky e +grid $check -pady 4 -row 2 -column 1 -sticky w +grid [ttk::label $buttonFrame.radioLabel -text "Radio Buttons:"]\ + -row 3 -column 0 -padx 4 -sticky e +grid $radio -pady 4 -row 3 -column 1 -sticky w +grid [ttk::label $buttonFrame.triangleLabel -text "Disclosure Triangle:"]\ + -row 4 -column 0 -padx 4 -sticky e +grid $triangle -pady 4 -row 4 -column 1 -sticky w +grid [ttk::label $buttonFrame.disclosureLabel -text "Disclosure Button:"]\ + -row 5 -column 0 -padx 4 -sticky e +grid $disclosure -row 5 -column 1 -sticky w +grid [ttk::label $buttonFrame.imageLabel -text "Image Buttons:"]\ + -row 7 -column 0 -padx 4 -sticky e +grid $bonjour -pady 4 -row 6 -rowspan 4 -column 1 -sticky w +grid $feather -padx 10 -pady 4 -row 6 -rowspan 4 -column 2 +grid [ttk::label $buttonFrame.gradentLabel -text "Gradient Buttons:"]\ +-row 10 -column 0 -padx 4 -sticky e +grid $gradient -pady 4 -row 10 -column 1 -sticky w +grid [ttk::label $buttonFrame.helpLabel -text "Help Button:"]\ +-row 11 -column 0 -padx 4 -sticky e +grid $help -row 11 -column 1 -sticky w + +#ttk::button .f.b1 -style Toolbutton -image action +#pack $buttonFrame + +# Entries Frame +set entryFrame [ttk::frame $w.notebook.entries -padding {0 30 80 0}] +grid columnconfigure $entryFrame 0 -minsize 200 +$w.notebook add $entryFrame -text "Entries" + +set textfield [ttk::entry $entryFrame.text -width 17] +set searchfield [ttk::entry $entryFrame.search -width 1] +set combo [ttk::combobox $entryFrame.combo -width 1 -textvariable comboVar \ + -values {"Item 1" "Item 2" "Item 3"}] +set stepper [ttk::spinbox $entryFrame.stepper -width 1 -textvariable stepVar \ + -from 99000 -to 101000 -increment 1] +set stepVar 100000 +searchFocusOut $searchfield +bind $searchfield <FocusIn> {searchFocusIn %W} +bind $searchfield <FocusOut> {searchFocusOut %W} + +grid [ttk::label $entryFrame.l0 -text "Text Field"] -row 0 -column 0 -padx 20 -sticky e +grid $textfield -sticky ew -row 0 -column 1 -pady 13 +grid [ttk::label $entryFrame.l1 -text "Search Field"] -row 1 -column 0 -padx 20 -sticky e +grid $searchfield -sticky ew -row 1 -column 1 -pady 13 +grid [ttk::label $entryFrame.l2 -text "Combo Box"] -row 2 -column 0 -padx 20 -sticky e +grid $combo -sticky ew -row 2 -column 1 -pady 13 +grid [ttk::label $entryFrame.l3 -text "Stepper"] -row 3 -column 0 -padx 20 -sticky e +grid $stepper -sticky ew -row 3 -column 1 -pady 13 + +#Scales Frame +set scaleFrame [ttk::frame $w.notebook.scales -padding {0 40 0 80}] +$w.notebook add $scaleFrame -text "Scales" + +variable topVar 50 +set topSlider [ttk::scale $scaleFrame.topSlider -from 0 -to 100 \ + -length 280 -variable topVar] +set topProgress [ttk::progressbar $scaleFrame.topProgress \ + -maximum 100 -variable topVar] + +variable bottomVar 50 +set bottomSlider [ttk::scale $scaleFrame.bottomSlider -from 0 -to 100 \ + -length 280 -variable bottomVar] +$bottomSlider state alternate +set bottomProgress [ttk::progressbar $scaleFrame.bottomProgress \ + -maximum 100 -variable bottomVar] + + +grid $topSlider -padx 80 -pady 12 -sticky ew -row 0 -column 0 -columnspan 2 +grid $topProgress -padx 120 -pady 15 -sticky ew -row 1 -column 0 -columnspan 2 +grid [ttk::frame $scaleFrame.spacer] -row 2 -column 0 -columnspan 2 -pady 32 + +grid $bottomSlider -padx 80 -sticky new -row 3 -column 0 -columnspan 2 +grid [ttk::label $scaleFrame.low -text Low -padding {70 0 0 0}] \ + -row 4 -column 0 -sticky sw +grid [ttk::label $scaleFrame.high -text High -padding {0 0 70 0}] \ + -row 4 -column 1 -sticky se +grid $bottomProgress -padx 120 -pady 15 -sticky ew -row 5 -column 0 -columnspan 2 + +#Appearance Frame +set appearanceFrame [ttk::frame $w.notebook.appearance -padding {0 40 0 80}] +grid [ttk::label $w.notebook.appearance.info -justify left -padding {0 20 0 40}\ + -text "Use the image buttons below to view this demo in light or dark mode."] \ + -row 0 -column 0 -columnspan 3 +set light [ttk::button $appearanceFrame.light -style ImageButton -text Light \ + -image {field pressed field1 selected field2} \ + -command "beLight $appearanceFrame $w"] +grid columnconfigure $appearanceFrame 1 -minsize 10 +grid $light -row 1 -column 0 -sticky e +set dark [ttk::button $appearanceFrame.dark -style ImageButton -text Dark \ + -image {starry pressed starry1 selected starry2} \ + -command "beDark $appearanceFrame $w"] +grid $dark -row 1 -column 2 -sticky w +if { [::tk::unsupported::MacWindowStyle isdark $w] } { + $dark state selected +} else { + $light state selected +} +proc beLight {f w} { + ::tk::unsupported::MacWindowStyle appearance $w aqua + $f.dark state !selected + $f.light state selected + after 10 $f.light state !hover +} + +proc beDark {f w} { + ::tk::unsupported::MacWindowStyle appearance $w darkaqua + $f.light state !selected + $f.dark state selected + after 10 $f.dark state !hover +} +$w.notebook add $appearanceFrame -text "Appearance" + +## See Code / Dismiss +pack [addSeeDismiss $w.buttons $w] -side bottom -fill x + +## Notebook +pack $w.notebook -side bottom -fill both -expand 1 -padx 16 -pady 16 + diff --git a/library/demos/widget b/library/demos/widget index 354ac3f..055dd60 100644 --- a/library/demos/widget +++ b/library/demos/widget @@ -329,7 +329,14 @@ addFormattedText { @@demo image2 A simple user interface for viewing images @@demo labelframe Labelled frames @@demo ttkbut The simple Themed Tk widgets - +} +if {[tk windowingsystem] eq "aqua"} { + addFormattedText { + @@new + @@demo mac_styles Special widgets for macOS + } +} +addFormattedText { @@subtitle Listboxes and Trees @@demo states The 50 states @@demo colors Colors: change the color scheme for the application diff --git a/library/ttk/aquaTheme.tcl b/library/ttk/aquaTheme.tcl index 7328cf1..00bb686 100644 --- a/library/ttk/aquaTheme.tcl +++ b/library/ttk/aquaTheme.tcl @@ -26,15 +26,55 @@ namespace eval ttk::theme::aqua { !focus systemSelectedTextColor} # Button - ttk::style configure TButton -anchor center -width -6 \ + ttk::style configure TButton -anchor center \ -foreground systemControlTextColor ttk::style map TButton \ -foreground { pressed white - {alternate !pressed !background} white} + {alternate !pressed !background} white + disabled systemDisabledControlTextColor} + + # Menubutton ttk::style configure TMenubutton -anchor center -padding {2 0 0 2} + + # Toolbutton ttk::style configure Toolbutton -anchor center + # Inline Button + ttk::style configure InlineButton -anchor center -font TkHeadingFont \ + -foreground systemTextBackgroundColor + ttk::style map InlineButton \ + -foreground { + disabled systemWindowBackgroundColor + } + + # Image Button + ttk::style configure ImageButton -anchor center -width 1 \ + -compound top + ttk::style map ImageButton \ + -foreground { + pressed systemLabelColor + !pressed systemSecondaryLabelColor + } + + # Recessed (radio) button + font create RecessedFont -family EmphasizedSystem -size 11 -weight bold + ttk::style configure RecessedButton \ + -foreground systemControlTextColor + ttk::style map RecessedButton \ + -foreground { + {disabled selected} systemWindowBackgroundColor3 + {disabled !selected} systemDisabledControlTextColor + selected systemTextBackgroundColor + active white + pressed white + } \ + -font { + selected RecessedFont + active RecessedFont + pressed RecessedFont + } + # For Entry, Combobox and Spinbox widgets the selected text background # is the "Highlight color" selected in preferences when the widget # has focus. It is a gray color when the widget does not have focus or @@ -86,9 +126,10 @@ namespace eval ttk::theme::aqua { ttk::style configure TNotebook.Tab -foreground systemControlTextColor ttk::style map TNotebook.Tab \ -foreground { - background systemControlTextColor - disabled systemDisabledControlTextColor - selected systemSelectedTabTextColor} + {background !selected} systemControlTextColor + {background selected} black + {!background selected} systemSelectedTabTextColor + disabled systemDisabledControlTextColor} # Treeview: ttk::style configure Heading \ @@ -113,7 +154,11 @@ namespace eval ttk::theme::aqua { # (ref: Apple Human Interface Guidelines / Controls / Grouping Controls) # ttk::style configure TLabelframe \ - -labeloutside true -labelmargins {14 0 14 4} + -labeloutside true \ + -labelmargins {14 0 14 2} + + ttk::style configure TLabelframe.Label \ + -font TkSmallCaptionFont # TODO: panedwindow sashes should be 9 pixels (HIG:Controls:Split Views) } diff --git a/library/ttk/combobox.tcl b/library/ttk/combobox.tcl index 8df4172..653102e 100644 --- a/library/ttk/combobox.tcl +++ b/library/ttk/combobox.tcl @@ -16,20 +16,17 @@ # window managers (even though the older ICCCM spec says # it's meaningless). # -# On OSX: [wm transient] does utterly the wrong thing. -# Instead, we use [MacWindowStyle "help" "noActivates hideOnSuspend"]. -# The "noActivates" attribute prevents the parent toplevel -# from deactivating when the popdown is posted, and is also -# necessary for "help" windows to receive mouse events. -# "hideOnSuspend" makes the popdown disappear (resp. reappear) -# when the parent toplevel is deactivated (resp. reactivated). -# (see [#1814778]). Also set [wm resizable 0 0], to prevent -# TkAqua from shrinking the scrollbar to make room for a grow box -# that isn't there. -# -# In order to work around other platform quirks in TkAqua, -# [grab] and [focus] are set in <Map> bindings instead of -# immediately after deiconifying the window. +# On OSX: The native combobox uses a popup menu to display the +# combobox choices. So this implementation does that as well, +# rather than construc a Tk listbox. Since the window manager +# takes care of scrolling and making sure that the menu can be +# displayed even when the button is close to the bottom of the +# screen, this actually simplifies the implementation. The Post +# and PopupWindow procs have separate implementations for Aqua +# and other systems. The configuration of the menu is handled +# by a different proc than the one which configures the listbox +# on other platforms -- ConfigureAquaMenu instead of +# ConfigureListbox. # namespace eval ttk::combobox { @@ -76,6 +73,9 @@ switch -- [tk windowingsystem] { # NB: *only* do this on Windows (see #1814778) bind ComboboxListbox <FocusOut> { ttk::combobox::LBCancel %W } } + aqua { + bind TCombobox <Destroy> { ttk::combobox::AquaCleanup %W } + } } ### Combobox popdown window bindings. @@ -98,9 +98,6 @@ switch -- [tk windowingsystem] { x11 { option add *TCombobox*Listbox.background white widgetDefault } - aqua { - option add *TCombobox*Listbox.borderWidth 0 widgetDefault - } } ### Binding procedures. @@ -264,34 +261,52 @@ proc ttk::combobox::UnmapPopdown {w} { # Returns the popdown widget associated with a combobox, # creating it if necessary. # -proc ttk::combobox::PopdownWindow {cb} { - if {![winfo exists $cb.popdown]} { - set poplevel [PopdownToplevel $cb.popdown] - set popdown [ttk::frame $poplevel.f -style ComboboxPopdownFrame] - - ttk::scrollbar $popdown.sb \ - -orient vertical -command [list $popdown.l yview] - listbox $popdown.l \ - -listvariable ttk::combobox::Values($cb) \ - -yscrollcommand [list $popdown.sb set] \ - -exportselection false \ - -selectmode browse \ - -activestyle none \ - ; - - bindtags $popdown.l \ - [list $popdown.l ComboboxListbox Listbox $popdown all] - - grid $popdown.l -row 0 -column 0 -padx {1 0} -pady 1 -sticky nsew - grid $popdown.sb -row 0 -column 1 -padx {0 1} -pady 1 -sticky ns - grid columnconfigure $popdown 0 -weight 1 - grid rowconfigure $popdown 0 -weight 1 - - grid $popdown -sticky news -padx 0 -pady 0 - grid rowconfigure $poplevel 0 -weight 1 - grid columnconfigure $poplevel 0 -weight 1 + +if {[tk windowingsystem] ne "aqua"} { + proc ttk::combobox::PopdownWindow {cb} { + if {![winfo exists $cb.popdown]} { + set poplevel [PopdownToplevel $cb.popdown] + set popdown [ttk::frame $poplevel.f -style ComboboxPopdownFrame] + + ttk::scrollbar $popdown.sb \ + -orient vertical -command [list $popdown.l yview] + listbox $popdown.l \ + -listvariable ttk::combobox::Values($cb) \ + -yscrollcommand [list $popdown.sb set] \ + -exportselection false \ + -selectmode browse \ + -activestyle none \ + ; + + bindtags $popdown.l \ + [list $popdown.l ComboboxListbox Listbox $popdown all] + + grid $popdown.l -row 0 -column 0 -padx {1 0} -pady 1 -sticky nsew + grid $popdown.sb -row 0 -column 1 -padx {0 1} -pady 1 -sticky ns + grid columnconfigure $popdown 0 -weight 1 + grid rowconfigure $popdown 0 -weight 1 + + grid $popdown -sticky news -padx 0 -pady 0 + grid rowconfigure $poplevel 0 -weight 1 + grid columnconfigure $poplevel 0 -weight 1 + } + return $cb.popdown + } +} else { + proc ttk::combobox::PopdownWindow {cb} { + if {![winfo exists $cb.popdown]} { + set poplevel [PopdownToplevel $cb.popdown] + # The menu should be (at least) the same length as the button. + # Since there is no direct way to control the width of a menu + # in Tk, we fake it by using an invisible image in a disabled + # menu item, adjusting the image size to make the menu be the + # correct width. + image create nsimage $cb.spacer -source NSStatusNone -as name \ + -alpha 0 + set menu [menu $cb.popdown.menu -tearoff 0] + } + return $cb.popdown } - return $cb.popdown } ## PopdownToplevel -- Create toplevel window for the combobox popdown @@ -314,10 +329,8 @@ proc ttk::combobox::PopdownToplevel {w} { wm attributes $w -topmost 1 } aqua { - $w configure -relief solid -borderwidth 0 - tk::unsupported::MacWindowStyle style $w \ - help {noActivates hideOnSuspend} - wm resizable $w 0 0 + wm overrideredirect $w true + wm attributes $w -alpha 0 } } return $w @@ -353,6 +366,39 @@ proc ttk::combobox::ConfigureListbox {cb} { $popdown.l configure -height $height } +proc ttk::combobox::ConfigureAquaMenu {cb width} { + set popdown [PopdownWindow $cb] + set values [$cb cget -values] + set current [$cb current] + if {$current < 0} { + set current 0 ;# no current entry, highlight first one + } + $cb.popdown.menu delete 0 end + $cb.spacer configure -width [expr {$width - 40}] -height 1 + set i 0 + foreach item $values { + if {$i == 0} { + # Add spaces to the first item to make the menu as long as cb + set menufont [$cb cget -font] + set stretch $item + while {[font measure $menufont $stretch] < [expr {$width - 32}]} { + set stretch "$stretch " + } + $cb.popdown.menu add command -label "$stretch" \ + -command "ttk::combobox::SelectEntry $cb $i" + } else { + $cb.popdown.menu add command -label "$item" \ + -command "ttk::combobox::SelectEntry $cb $i" + } + incr i + } + if { $i == 0 } { + # There are no items. To make an empty menu appear add a dummy item + # containing a transparent image of the right width. + $cb.popdown.menu add command -label {} -image $cb.spacer -state disabled + } +} + ## PlacePopdown -- # Set popdown window geometry. # @@ -381,34 +427,75 @@ proc ttk::combobox::PlacePopdown {cb popdown} { wm geometry $popdown ${w}x${H}+${x}+${Y} } +proc ttk::combobox::AquaPlacePopdown {cb popdown} { + set x [winfo rootx $cb] + set y [winfo rooty $cb] + set w [winfo width $cb] + set h [winfo height $cb] + set style [$cb cget -style] + set postoffset [ttk::style lookup $style -postoffset {} {0 0 0 0}] + foreach var {x y w h} delta $postoffset { + incr $var $delta + } + wm geometry $popdown ${w}x${h}+${x}+${y} + return [list $x $y $w $h] +} + ## Post $cb -- -# Pop down the associated listbox. -# -proc ttk::combobox::Post {cb} { - # Don't do anything if disabled: - # - $cb instate disabled { return } +# Pop down the associated listbox or menu. +# +if {[tk windowingsystem] ne "aqua"} { + proc ttk::combobox::Post {cb} { + # Don't do anything if disabled: + # + $cb instate disabled { return } + + # ASSERT: ![$cb instate pressed] + + # Run -postcommand callback: + # + uplevel #0 [$cb cget -postcommand] + + set popdown [PopdownWindow $cb] + ConfigureListbox $cb + update idletasks ;# needed for geometry propagation. + PlacePopdown $cb $popdown + # See <<NOTE-WM-TRANSIENT>> + switch -- [tk windowingsystem] { + x11 - win32 { wm transient $popdown [winfo toplevel $cb] } + } - # ASSERT: ![$cb instate pressed] + # Post the listbox: + # + wm attribute $popdown -topmost 1 + wm deiconify $popdown + raise $popdown + } +} else { + proc ttk::combobox::Post {cb} { + # Don't do anything if disabled: + # + $cb instate disabled { return } - # Run -postcommand callback: - # - uplevel #0 [$cb cget -postcommand] + # ASSERT: ![$cb instate pressed] - set popdown [PopdownWindow $cb] - ConfigureListbox $cb - update idletasks ;# needed for geometry propagation. - PlacePopdown $cb $popdown - # See <<NOTE-WM-TRANSIENT>> - switch -- [tk windowingsystem] { - x11 - win32 { wm transient $popdown [winfo toplevel $cb] } - } + # Run -postcommand callback: + # + uplevel #0 [$cb cget -postcommand] + + set popdown [PopdownWindow $cb] + + # Configure the menu - # Post the listbox: - # - wm attribute $popdown -topmost 1 - wm deiconify $popdown - raise $popdown + foreach {x y width height} [AquaPlacePopdown $cb $popdown] { break } + ConfigureAquaMenu $cb [winfo width $cb] + + # Post the menu. It will have a disclosure indicator if it is too + # close to the bottom of the screen, and it may be posted above the + # button if necessary to be visible. + + $popdown.menu post [expr {$x + 2}] [expr {$y + $height + 2}] + } } ## Unpost $cb -- @@ -452,4 +539,8 @@ proc ttk::combobox::LBCleanup {lb} { unset Values([LBMain $lb]) } +proc ttk::combobox::AquaCleanup {cb} { + catch {image delete $cb.spacer} +} + #*EOF* diff --git a/library/ttk/menubutton.tcl b/library/ttk/menubutton.tcl index cfc2690..cd34690 100644 --- a/library/ttk/menubutton.tcl +++ b/library/ttk/menubutton.tcl @@ -57,7 +57,7 @@ if {[tk windowingsystem] eq "x11"} { bind TMenubutton <Button-1> \ { %W state pressed ; ttk::menubutton::Popdown %W } bind TMenubutton <ButtonRelease-1> \ - { if {[winfo exists %W]} { %W state !pressed } } + { if {[winfo exists %W]} { %W state {!pressed}} } } # PostPosition -- @@ -77,6 +77,7 @@ if {[tk windowingsystem] eq "aqua"} { set menuPad 5 set buttonPad 1 set bevelPad 4 + set flushPad 4 set mh [winfo reqheight $menu] set bh [expr {[winfo height $mb]} + $buttonPad] set bbh [expr {[winfo height $mb]} + $bevelPad] @@ -105,8 +106,11 @@ if {[tk windowingsystem] eq "aqua"} { incr y $menuPad incr x $bw } - default { # flush - incr y $bbh + flush { + incr y $flushPad + incr x -$flushPad + } + default { } } return [list $x $y $entry] diff --git a/library/ttk/progress.tcl b/library/ttk/progress.tcl index 1729e11..929f0e6 100644 --- a/library/ttk/progress.tcl +++ b/library/ttk/progress.tcl @@ -32,6 +32,9 @@ proc ttk::progressbar::start {pb {steptime 50} {stepsize 1}} { if {![info exists Timers($pb)]} { Autoincrement $pb $steptime $stepsize } + if {[tk windowingsystem] eq "aqua"} { + $pb state selected + } } # ttk::progressbar::stop -- @@ -44,6 +47,9 @@ proc ttk::progressbar::stop {pb} { unset Timers($pb) } $pb configure -value 0 + if {[tk windowingsystem] eq "aqua"} { + $pb state !selected + } } diff --git a/macosx/tkMacOSXConstants.h b/macosx/tkMacOSXConstants.h index 2341cb8..29bc391 100644 --- a/macosx/tkMacOSXConstants.h +++ b/macosx/tkMacOSXConstants.h @@ -79,6 +79,9 @@ typedef NSInteger NSModalResponse; #define NSCursorUpdate NSEventTypeCursorUpdate #define NSTexturedBackgroundWindowMask NSWindowStyleMaskTexturedBackground #define NSCompositeCopy NSCompositingOperationCopy +#define NSCompositeSourceOver NSCompositingOperationSourceOver +#define NSCompositeSourceAtop NSCompositingOperationSourceAtop +#define NSCompositeDestinationIn NSCompositingOperationDestinationIn #define NSWarningAlertStyle NSAlertStyleWarning #define NSInformationalAlertStyle NSAlertStyleInformational #define NSCriticalAlertStyle NSAlertStyleCritical diff --git a/macosx/tkMacOSXDraw.c b/macosx/tkMacOSXDraw.c index 4779c9a..69c0dc5 100644 --- a/macosx/tkMacOSXDraw.c +++ b/macosx/tkMacOSXDraw.c @@ -133,7 +133,7 @@ TkMacOSXGetNSImageFromTkImage( { Pixmap pixmap; NSImage *nsImage; - if (width == 0 || height == 0) { + if (width <= 0 || height <= 0) { return nsImage = [[NSImage alloc] initWithSize:NSMakeSize(0,0)]; } pixmap = Tk_GetPixmap(display, None, width, height, 0); diff --git a/macosx/tkMacOSXImage.c b/macosx/tkMacOSXImage.c index 0b63148..00135c5 100644 --- a/macosx/tkMacOSXImage.c +++ b/macosx/tkMacOSXImage.c @@ -1,7 +1,8 @@ /* * tkMacOSXImage.c -- * - * The code in this file provides an interface for XImages, + * The code in this file provides an interface for XImages, and + * implements the nsimage image type. * * Copyright (c) 1995-1997 Sun Microsystems, Inc. * Copyright (c) 2001-2009, Apple Inc. @@ -14,6 +15,7 @@ #include "tkMacOSXPrivate.h" #include "tkMacOSXConstants.h" +#include "tkColor.h" #include "xbytes.h" static CGImageRef CreateCGImageFromPixmap(Drawable pixmap); @@ -114,7 +116,7 @@ _XInitImageFuncPtrs( static void ReleaseData( void *info, TCL_UNUSED(const void *), /* data */ - TCL_UNUSED(size_t)) /* size */ + TCL_UNUSED(size_t)) /* size */ { ckfree(info); } @@ -388,7 +390,7 @@ ImagePutPixel( XImage * XCreateImage( Display* display, - TCL_UNUSED(Visual*), /* visual */ + TCL_UNUSED(Visual*), /* visual */ unsigned int depth, int format, int offset, @@ -732,7 +734,7 @@ XGetImage( int y, unsigned int width, unsigned int height, - TCL_UNUSED(unsigned long), /* plane_mask */ + TCL_UNUSED(unsigned long), /* plane_mask */ int format) { NSBitmapImageRep* bitmapRep = nil; @@ -1011,6 +1013,712 @@ XCopyPlane( } } +/* ---------------------------------------------------------------------------*/ + +/* + * Implementation of a Tk image type which provide access to NSImages + * for use in buttons etc. + */ + +/* + * Forward declarations. + */ + +typedef struct TkMacOSXNSImageInstance TkMacOSXNSImageInstance; +typedef struct TkMacOSXNSImageModel TkMacOSXNSImageModel; + +/* + * The following data structure represents a particular use of an nsimage + * in a widget. + */ + +struct TkMacOSXNSImageInstance { + TkMacOSXNSImageModel *modelPtr; /* Pointer to the model for the image. */ + NSImage *image; /* Pointer to an NSImage.*/ + TkMacOSXNSImageInstance *nextPtr; /* First in the list of instances associated + * with this model. */ +}; + +/* + * The following data structure represents the model for an nsimage: + */ + +struct TkMacOSXNSImageModel { + Tk_ImageModel tkModel; /* Tk's token for image model. */ + Tcl_Interp *interp; /* Interpreter for application. */ + int width, height; /* Dimensions of the image. */ + int radius; /* Radius for rounded corners. */ + int ring; /* Thickness of the focus ring. */ + double alpha; /* Transparency, between 0.0 and 1.0*/ + bool pressed; /* Image is for use in a pressed button.*/ + bool template; /* Image is for use as a template.*/ + char *imageName ; /* Malloc'ed image name. */ + char *source; /* Malloc'ed string describing the image. */ + char *as; /* Malloc'ed interpretation of source */ + int flags; /* Sundry flags, defined below. */ + TkMacOSXNSImageInstance *instancePtr; /* Start of list of instances associated + * with this model. */ + NSImage *image; /* The underlying NSImage object. */ + NSImage *darkModeImage; /* A modified image to use in Dark Mode. */ +}; + +/* + * Bit definitions for the flags field of a TkMacOSXNSImageModel. + * IMAGE_CHANGED: 1 means that the instances of this image need + * to be redisplayed. + */ + +#define IMAGE_CHANGED 1 + +/* + * The type record for nsimage images: + */ + +static int TkMacOSXNSImageCreate(Tcl_Interp *interp, + const char *name, int argc, Tcl_Obj *const objv[], + const Tk_ImageType *typePtr, Tk_ImageModel model, + ClientData *clientDataPtr); +static ClientData TkMacOSXNSImageGet(Tk_Window tkwin, ClientData clientData); +static void TkMacOSXNSImageDisplay(ClientData clientData, + Display *display, Drawable drawable, + int imageX, int imageY, int width, + int height, int drawableX, + int drawableY); +static void TkMacOSXNSImageFree(ClientData clientData, Display *display); +static void TkMacOSXNSImageDelete(ClientData clientData); + +static Tk_ImageType TkMacOSXNSImageType = { + "nsimage", /* name of image type */ + TkMacOSXNSImageCreate, /* createProc */ + TkMacOSXNSImageGet, /* getProc */ + TkMacOSXNSImageDisplay, /* displayProc */ + TkMacOSXNSImageFree, /* freeProc */ + TkMacOSXNSImageDelete, /* deleteProc */ + NULL, /* postscriptPtr */ + NULL, /* nextPtr */ + NULL +}; + +/* + * Default values used for parsing configuration specifications: + */ +#define DEF_SOURCE "" +#define DEF_AS "name" +#define DEF_HEIGHT "0" +#define DEF_WIDTH "0" +#define DEF_RADIUS "0" +#define DEF_RING "0" +#define DEF_ALPHA "1.0" +#define DEF_PRESSED "0" +#define DEF_TEMPLATE "0" + +static const Tk_OptionSpec systemImageOptions[] = { + {TK_OPTION_STRING, "-source", NULL, NULL, DEF_SOURCE, + -1, Tk_Offset(TkMacOSXNSImageModel, source), 0, NULL, 0}, + {TK_OPTION_STRING, "-as", NULL, NULL, DEF_AS, + -1, Tk_Offset(TkMacOSXNSImageModel, as), 0, NULL, 0}, + {TK_OPTION_INT, "-width", NULL, NULL, DEF_WIDTH, + -1, Tk_Offset(TkMacOSXNSImageModel, width), 0, NULL, 0}, + {TK_OPTION_INT, "-height", NULL, NULL, DEF_HEIGHT, + -1, Tk_Offset(TkMacOSXNSImageModel, height), 0, NULL, 0}, + {TK_OPTION_INT, "-radius", NULL, NULL, DEF_RADIUS, + -1, Tk_Offset(TkMacOSXNSImageModel, radius), 0, NULL, 0}, + {TK_OPTION_INT, "-ring", NULL, NULL, DEF_RING, + -1, Tk_Offset(TkMacOSXNSImageModel, ring), 0, NULL, 0}, + {TK_OPTION_DOUBLE, "-alpha", NULL, NULL, DEF_ALPHA, + -1, Tk_Offset(TkMacOSXNSImageModel, alpha), 0, NULL, 0}, + {TK_OPTION_BOOLEAN, "-pressed", NULL, NULL, DEF_PRESSED, + -1, Tk_Offset(TkMacOSXNSImageModel, pressed), 0, NULL, 0}, + {TK_OPTION_BOOLEAN, "-template", NULL, NULL, DEF_TEMPLATE, + -1, Tk_Offset(TkMacOSXNSImageModel, pressed), 0, NULL, 0}, + {TK_OPTION_END, NULL, NULL, NULL, NULL, 0, -1, 0, NULL, 0} +}; + +/* + * The -as option specifies how the string provided in the -source + * option should be interpreted as a description of an NSImage. + * Below are the possible values and their meanings. (The last two + * provide the macOS icon for a particular file type.) + */ + +static const char *sourceInterpretations[] = { + "name", /* A name for a named NSImage. */ + "file", /* A path to an image file. */ + "path", /* A path to a file whose type should be examined. */ + "filetype", /* A file extension or 4-byte OSCode. */ +}; + +enum {NAME_SOURCE, FILE_SOURCE, PATH_SOURCE, FILETYPE_SOURCE}; + + +/* + *---------------------------------------------------------------------- + * + * TintImage -- + * + * Modify an NSImage by blending it with a color. The transparent part of + * the image remains transparent. The opaque part of the image is painted + * with the color, using the specified alpha value for the transparency of + * the color. + * + * Results: + * None. + * + * Side effects: + * The appearance of the NSImage changes. + * + *---------------------------------------------------------------------- + */ + +static void TintImage( + NSImage *image, + NSColor *color, + double alpha) +{ + NSSize size = [image size]; + NSRect rect = {NSZeroPoint, size}; + NSImage *mask = [[[NSImage alloc] initWithSize:size] retain]; + [mask lockFocus]; + [color set]; + NSRectFillUsingOperation(rect, NSCompositeCopy); + [image drawInRect:rect + fromRect:rect + operation:NSCompositeDestinationIn + fraction:1.0]; + [mask unlockFocus]; + [image lockFocus]; + [mask drawInRect:rect + fromRect:rect + operation:NSCompositeSourceOver + fraction:alpha]; + [image unlockFocus]; + [mask release]; +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImageConfigureModel -- + * + * This function is called when an nsimage image is created or + * reconfigured. It processes configuration options and resets any + * instances of the image. + * + * Results: + * A standard Tcl return value. If TCL_ERROR is returned then an error + * message is left in the modelPtr->interp's result. + * + * Side effects: + * Existing instances of the image will be redisplayed to match the new + * configuration options. + * + *---------------------------------------------------------------------- + */ + +static int +TkMacOSXNSImageConfigureModel( + Tcl_Interp *interp, /* Interpreter to use for reporting errors. */ + TkMacOSXNSImageModel *modelPtr, /* Pointer to data structure describing + * overall photo image to (re)configure. */ + int objc, /* Number of entries in objv. */ + Tcl_Obj *const objv[]) /* Pairs of configuration options for image. */ +{ + Tk_OptionTable optionTable = Tk_CreateOptionTable(interp, systemImageOptions); + NSImage *newImage; + Tcl_Obj *objPtr; + static Tcl_Obj *asOption = NULL; + int sourceInterpretation; + NSString *source; + int oldWidth = modelPtr->width, oldHeight = modelPtr->height; + + if (asOption == NULL) { + asOption = Tcl_NewStringObj("-as", -1); + Tcl_IncrRefCount(asOption); + } + + modelPtr->width = 0; + modelPtr->height = 0; + if (Tk_SetOptions(interp, (char *) modelPtr, optionTable, objc, objv, + NULL, NULL, NULL) != TCL_OK){ + goto errorExit; + } + if (modelPtr->width == 0 && modelPtr->height == 0) { + modelPtr->width = oldWidth; + modelPtr->height = oldHeight; + } + + if (modelPtr->source == NULL || modelPtr->source[0] == '0') { + Tcl_SetObjResult(interp, Tcl_NewStringObj("-source is required.", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SYSTEM", "BAD_VALUE", NULL); + goto errorExit; + } + + objPtr = Tk_GetOptionValue(interp, (char *) modelPtr, optionTable, + asOption, NULL); + if (Tcl_GetIndexFromObj(interp, objPtr, sourceInterpretations, "option", + 0, &sourceInterpretation) != TCL_OK) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "Unknown interpretation for source in -as option. " + "Should be name, file, path, or filetype.", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SYSTEM", "BAD_VALUE", NULL); + goto errorExit; + } + + source = [[NSString alloc] initWithUTF8String: modelPtr->source]; + switch (sourceInterpretation) { + case NAME_SOURCE: + newImage = [[NSImage imageNamed:source] copy]; + break; + case FILE_SOURCE: + newImage = [[NSImage alloc] initWithContentsOfFile:source]; + break; + case PATH_SOURCE: + newImage = [[NSWorkspace sharedWorkspace] iconForFile:source]; + break; + case FILETYPE_SOURCE: + newImage = TkMacOSXIconForFileType(source); + break; + default: + newImage = NULL; + break; + } + [source release]; + if (newImage) { + NSSize size = NSMakeSize(modelPtr->width - 2*modelPtr->ring, + modelPtr->height - 2*modelPtr->ring); + [modelPtr->image release]; + [modelPtr->darkModeImage release]; + newImage.size = size; + modelPtr->image = [newImage retain]; + if (modelPtr->template) { + newImage.template = YES; + } + modelPtr->darkModeImage = [[newImage copy] retain]; + if ([modelPtr->darkModeImage isTemplate]) { + + /* + * For a template image the Dark Mode version should be white. + */ + + NSRect rect = {NSZeroPoint, size}; + [modelPtr->darkModeImage lockFocus]; + [[NSColor whiteColor] set]; + NSRectFillUsingOperation(rect, NSCompositeSourceAtop); + [modelPtr->darkModeImage unlockFocus]; + } else if (modelPtr->pressed) { + + /* + * Non-template pressed images are darker in Light Mode and lighter + * in Dark Mode. + */ + + TintImage(modelPtr->image, [NSColor blackColor], 0.2); + TintImage(modelPtr->darkModeImage, [NSColor whiteColor], 0.5); + } + } else { + switch(sourceInterpretation) { + case NAME_SOURCE: + Tcl_SetObjResult(interp, Tcl_NewStringObj("Unknown named NSImage.\n" + "Try omitting ImageName, " + "e.g. use NSCaution for NSImageNameCaution.", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SYSTEM", "BAD_VALUE", NULL); + goto errorExit; + case FILE_SOURCE: + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "Failed to load image file.\n", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SYSTEM", "BAD_VALUE", NULL); + goto errorExit; + default: + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "Unrecognized file type.\n" + "If using a filename extension, do not include the dot.\n", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SYSTEM", "BAD_VALUE", NULL); + goto errorExit; + } + } + + /* + * Set the width and height. If only one is specified, set the other one + * so as to preserve the aspect ratio. If neither is specified, match the + * size of the image. + */ + + if (modelPtr->width == 0 && modelPtr->height == 0) { + CGSize size = [modelPtr->image size]; + modelPtr->width = (int) size.width; + modelPtr->height = (int) size.height; + } else { + CGSize size = [modelPtr->image size], newsize; + CGFloat aspect = size.width && size.height ? + size.height / size.width : 1; + if (modelPtr->width == 0) { + modelPtr->width = (int) ((CGFloat)(modelPtr->height) / aspect); + } else if (modelPtr->height == 0) { + modelPtr->height = (int) ((CGFloat)(modelPtr->width) * aspect); + } + newsize = NSMakeSize(modelPtr->width, modelPtr->height); + modelPtr->image.size = newsize; + modelPtr->darkModeImage.size = newsize; + } + + /* + * Inform the generic image code that the image has (potentially) changed. + */ + + Tk_ImageChanged(modelPtr->tkModel, 0, 0, modelPtr->width, + modelPtr->height, modelPtr->width, modelPtr->height); + modelPtr->flags &= ~IMAGE_CHANGED; + + return TCL_OK; + + errorExit: + return TCL_ERROR; +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImageObjCmd -- + * + * This function implements the configure and cget commands for an + * nsimage instance. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * The image may be reconfigured. + * + *---------------------------------------------------------------------- + */ + +int +TkMacOSXNSImageObjCmd( + ClientData clientData, /* Information about the image model. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + TkMacOSXNSImageModel *modelPtr = (TkMacOSXNSImageModel *)clientData; + Tk_OptionTable optionTable = Tk_CreateOptionTable(interp, systemImageOptions); + static const char *const options[] = {"cget", "configure", NULL}; + enum {CGET, CONFIGURE}; + Tcl_Obj *objPtr; + int index; + + if (objc < 2) { + Tcl_WrongNumArgs(interp, 1, objv, "option ?arg ...?"); + return TCL_ERROR; + } + if (Tcl_GetIndexFromObjStruct(interp, objv[1], options, + sizeof(char *), "option", 0, &index) != TCL_OK) { + return TCL_ERROR; + } + Tcl_Preserve(modelPtr); + switch (index) { + case CGET: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 2, objv, "option"); + return TCL_ERROR; + } + objPtr = Tk_GetOptionValue(interp, (char *)modelPtr, optionTable, + objv[2], NULL); + if (objPtr == NULL) { + goto error; + } + Tcl_SetObjResult(interp, objPtr); + break; + case CONFIGURE: + if (objc == 2) { + objPtr = Tk_GetOptionInfo(interp, (char *)modelPtr, optionTable, + NULL, NULL); + if (objPtr == NULL) { + goto error; + } + Tcl_SetObjResult(interp, objPtr); + break; + } else if (objc == 3) { + objPtr = Tk_GetOptionInfo(interp, (char *)modelPtr, optionTable, + objv[2], NULL); + if (objPtr == NULL) { + goto error; + } + Tcl_SetObjResult(interp, objPtr); + break; + } else { + TkMacOSXNSImageConfigureModel(interp, modelPtr, objc - 2, objv + 2); + break; + } + default: + break; + } + + Tcl_Release(modelPtr); + return TCL_OK; + + error: + Tcl_Release(modelPtr); + return TCL_ERROR; +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImageCreate -- + * + * Allocate and initialize an nsimage model. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * The data structure for a new image is allocated. + * + *---------------------------------------------------------------------- + */ + +static int +TkMacOSXNSImageCreate( + Tcl_Interp *interp, /* Interpreter for application using image. */ + const char *name, /* Name to use for image. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[], /* Argument strings for options (not + * including image name or type). */ + TCL_UNUSED(const Tk_ImageType *), /* typePtr */ + Tk_ImageModel model, /* Token for image, to be used in callbacks. */ + ClientData *clientDataPtr) /* Store manager's token for image here; it + * will be returned in later callbacks. */ +{ + TkMacOSXNSImageModel *modelPtr; + Tk_OptionTable optionTable = Tk_CreateOptionTable(interp, systemImageOptions); + + modelPtr = (TkMacOSXNSImageModel *)ckalloc(sizeof(TkMacOSXNSImageModel)); + modelPtr->tkModel = model; + modelPtr->interp = interp; + modelPtr->imageName = (char *)ckalloc(strlen(name) + 1); + strcpy(modelPtr->imageName, name); + modelPtr->flags = 0; + modelPtr->instancePtr = NULL; + modelPtr->image = NULL; + modelPtr->darkModeImage = NULL; + modelPtr->source = NULL; + modelPtr->as = NULL; + + /* + * Process configuration options given in the image create command. + */ + + if (Tk_InitOptions(interp, (char *) modelPtr, optionTable, NULL) != TCL_OK + || TkMacOSXNSImageConfigureModel(interp, modelPtr, objc, objv) != TCL_OK) { + TkMacOSXNSImageDelete(modelPtr); + return TCL_ERROR; + } + Tcl_CreateObjCommand(interp, name, TkMacOSXNSImageObjCmd, modelPtr, NULL); + *clientDataPtr = modelPtr; + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImageGet -- + * + * Allocate and initialize an nsimage instance. + * + * Results: + * The return value is a token for the image instance, which is used in + * future callbacks to ImageDisplay and ImageFree. + * + * Side effects: + * A new new nsimage instance is created. + * + *---------------------------------------------------------------------- + */ + +static ClientData +TkMacOSXNSImageGet( + TCL_UNUSED(Tk_Window), /* tkwin */ + ClientData clientData) /* Pointer to TkMacOSXNSImageModel for image. */ +{ + TkMacOSXNSImageModel *modelPtr = (TkMacOSXNSImageModel *) clientData; + TkMacOSXNSImageInstance *instPtr; + + instPtr = (TkMacOSXNSImageInstance *)ckalloc(sizeof(TkMacOSXNSImageInstance)); + instPtr->modelPtr = modelPtr; + return instPtr; +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImageDisplay -- + * + * Display or redisplay an nsimage in the given drawable. + * + * Results: + * None. + * + * Side effects: + * The image gets drawn. + * + *---------------------------------------------------------------------- + */ + +static void +TkMacOSXNSImageDisplay( + ClientData clientData, /* Pointer to TkMacOSXNSImageInstance for image. */ + TCL_UNUSED(Display *), /* display */ + Drawable drawable, /* Where to draw or redraw image. */ + int imageX, int imageY, /* Origin of area to redraw, relative to + * origin of image. */ + int width, int height, /* Dimensions of area to redraw. */ + int drawableX, int drawableY) + /* Coordinates in drawable corresponding to + * imageX and imageY. */ +{ + MacDrawable *macWin = (MacDrawable *) drawable; + Tk_Window tkwin = (Tk_Window) macWin->winPtr; + TkMacOSXNSImageInstance *instPtr = (TkMacOSXNSImageInstance *) clientData; + TkMacOSXNSImageModel *modelPtr = instPtr->modelPtr; + TkMacOSXDrawingContext dc; + NSRect dstRect = NSMakeRect(macWin->xOff + drawableX, + macWin->yOff + drawableY, width, height); + NSRect srcRect = NSMakeRect(imageX, imageY, width, height); + NSImage *image = TkMacOSXInDarkMode(tkwin) ? modelPtr->darkModeImage : + modelPtr->image; + int ring = modelPtr->ring; + int radius = modelPtr->radius; + + if (TkMacOSXSetupDrawingContext(drawable, NULL, &dc)) { + if (dc.context) { + CGRect clipRect = CGRectMake( + dstRect.origin.x - srcRect.origin.x + ring, + dstRect.origin.y - srcRect.origin.y + ring, + modelPtr->width - 2*ring, + modelPtr->height - 2*ring); + CGPathRef path = CGPathCreateWithRoundedRect(clipRect, radius, radius, NULL); + CGContextSaveGState(dc.context); + CGContextBeginPath(dc.context); + CGContextAddPath(dc.context, path); + CGContextClip(dc.context); + NSGraphicsContext *savedContext = NSGraphicsContext.currentContext; + NSGraphicsContext.currentContext = [NSGraphicsContext + graphicsContextWithCGContext:dc.context flipped:YES]; + [image drawInRect:clipRect + fromRect:srcRect + operation:NSCompositeSourceOver + fraction:modelPtr->alpha + respectFlipped:YES + hints:nil]; + CGContextRestoreGState(dc.context); + + /* + * Draw the focus ring. + */ + + if (ring) { + CGRect ringRect = CGRectInset(clipRect, -ring, -ring); + CGPathRef ringPath = CGPathCreateWithRoundedRect(ringRect, + radius + ring, radius + ring, NULL); + CGContextSaveGState(dc.context); + CGContextAddPath(dc.context, path); + CGContextAddPath(dc.context, ringPath); + CGContextSetFillColorWithColor(dc.context, + controlAccentColor().CGColor); + CGContextEOFillPath(dc.context); + CGContextRestoreGState(dc.context); + CFRelease(ringPath); + } + CFRelease(path); + NSGraphicsContext.currentContext = savedContext; + } + TkMacOSXRestoreDrawingContext(&dc); + } +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImageFree -- + * + * Deallocate an instance of an nsimage. + * + * Results: + * None. + * + * Side effects: + * Information related to the instance is freed. + * + *---------------------------------------------------------------------- + */ + +static void +TkMacOSXNSImageFree( + ClientData clientData, /* Pointer to TkMacOSXNSImageInstance for instance. */ + TCL_UNUSED(Display *)) /* display */ +{ + TkMacOSXNSImageInstance *instPtr = (TkMacOSXNSImageInstance *) clientData; + ckfree(instPtr); +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImageDelete -- + * + * Deallocate an nsimage model. + * + * Results: + * None. + * + * Side effects: + * NSImages are released and memory is freed. + * + *---------------------------------------------------------------------- + */ + +static void +TkMacOSXNSImageDelete( + ClientData clientData) /* Pointer to TkMacOSXNSImageModel for image. When + * this function is called, no more instances + * exist. */ +{ + TkMacOSXNSImageModel *modelPtr = (TkMacOSXNSImageModel *) clientData; + + Tcl_DeleteCommand(modelPtr->interp, modelPtr->imageName); + ckfree(modelPtr->imageName); + ckfree(modelPtr->source); + ckfree(modelPtr->as); + [modelPtr->image release]; + [modelPtr->darkModeImage release]; + ckfree(modelPtr); +} + +/* + *---------------------------------------------------------------------- + * + * TkMacOSXNSImage_Init -- + * + * Adds the TkMacOSXNSImage type to Tk. + * + * Results: + * Returns a standard Tcl completion code, and leaves an error message in + * the interp's result if an error occurs. + * + * Side effects: + * Creates the image create nsrect ... command. + * + *---------------------------------------------------------------------- + */ + +int +TkMacOSXNSImage_Init( + TCL_UNUSED(Tcl_Interp *)) /* interp */ +{ + Tk_CreateImageType(&TkMacOSXNSImageType); + return 1; +} + /* * Local Variables: * mode: objc diff --git a/macosx/tkMacOSXInit.c b/macosx/tkMacOSXInit.c index dd7b18d..8eb1fb6 100644 --- a/macosx/tkMacOSXInit.c +++ b/macosx/tkMacOSXInit.c @@ -33,7 +33,9 @@ static char scriptPath[PATH_MAX + 1] = ""; * Forward declarations... */ -static int TkMacOSXGetAppPathCmd(ClientData cd, Tcl_Interp *ip, +static int TkMacOSXGetAppPathObjCmd(TCL_UNUSED(void *), Tcl_Interp *ip, + int objc, Tcl_Obj *const objv[]); +static int TkMacOSVersionObjCmd(ClientData cd, Tcl_Interp *ip, int objc, Tcl_Obj *const objv[]); #pragma mark TKApplication(TKInit) @@ -42,7 +44,6 @@ static int TkMacOSXGetAppPathCmd(ClientData cd, Tcl_Interp *ip, @synthesize poolLock = _poolLock; @synthesize macOSVersion = _macOSVersion; @synthesize isDrawing = _isDrawing; -@synthesize needsToDraw = _needsToDraw; @synthesize isSigned = _isSigned; @synthesize tkLiveResizeEnded = _tkLiveResizeEnded; @synthesize tkPointerWindow = _tkPointerWindow; @@ -179,6 +180,7 @@ static int TkMacOSXGetAppPathCmd(ClientData cd, Tcl_Interp *ip, /* * Initialize the graphics context. */ + TkMacOSXUseAntialiasedText(_eventInterp, -1); TkMacOSXInitCGDrawing(_eventInterp, TRUE, 0); @@ -190,6 +192,12 @@ static int TkMacOSXGetAppPathCmd(ClientData cd, Tcl_Interp *ip, [self _setupMenus]; /* + * Run initialization routines that depend on the OS version. + */ + + Ttk_MacOSXInit(); + + /* * It is not safe to force activation of the NSApp until this method is * called. Activating too early can cause the menu bar to be unresponsive. * The call to activateIgnoringOtherApps was moved here to avoid this. @@ -597,12 +605,13 @@ TkpInit( } /* - * Initialize the NSServices object here. Apple's docs say to do this - * in applicationDidFinishLaunching, but the Tcl interpreter is not - * initialized until this function call. + * Now we can run initialization routines which require that both the + * NSApplication and the Tcl interpreter have been created and + * initialized. */ TkMacOSXServices_Init(interp); + TkMacOSXNSImage_Init(interp); /* * The root window has been created and mapped, but XMapWindow deferred its @@ -638,7 +647,6 @@ TkpInit( signal(SIGHUP, TkMacOSXSignalHandler); signal(SIGTERM, TkMacOSXSignalHandler); } - /* * Initialization steps that are needed for all interpreters. */ @@ -646,18 +654,20 @@ TkpInit( if (tkLibPath[0] != '\0') { Tcl_SetVar2(interp, "tk_library", NULL, tkLibPath, TCL_GLOBAL_ONLY); } - if (scriptPath[0] != '\0') { Tcl_SetVar2(interp, "auto_path", NULL, scriptPath, TCL_GLOBAL_ONLY|TCL_LIST_ELEMENT|TCL_APPEND_VALUE); } - + Tcl_CreateObjCommand(interp, "nsimage", + TkMacOSXNSImageObjCmd, NULL, NULL); Tcl_CreateObjCommand(interp, "::tk::mac::standardAboutPanel", TkMacOSXStandardAboutPanelObjCmd, NULL, NULL); Tcl_CreateObjCommand(interp, "::tk::mac::iconBitmap", TkMacOSXIconBitmapObjCmd, NULL, NULL); Tcl_CreateObjCommand(interp, "::tk::mac::GetAppPath", - TkMacOSXGetAppPathCmd, NULL, NULL); + TkMacOSXGetAppPathObjCmd, NULL, NULL); + Tcl_CreateObjCommand(interp, "::tk::mac::macOSVersion", + TkMacOSVersionObjCmd, NULL, NULL); MacSystrayInit(interp); MacPrint_Init(interp); @@ -667,6 +677,55 @@ TkpInit( /* *---------------------------------------------------------------------- * + * TkMacOSXGetAppPathObjCmd -- + * + * Returns the path of the Wish application bundle. + * + * Results: + * Returns the application path. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static int +TkMacOSXGetAppPathObjCmd( + TCL_UNUSED(void *), + Tcl_Interp *interp, + int objc, + Tcl_Obj *const objv[]) +{ + if (objc != 1) { + Tcl_WrongNumArgs(interp, 1, objv, NULL); + return TCL_ERROR; + } + + /* + * Get the application path URL and convert it to a string path reference. + */ + + CFURLRef mainBundleURL = CFBundleCopyBundleURL(CFBundleGetMainBundle()); + CFStringRef appPath = + CFURLCopyFileSystemPath(mainBundleURL, kCFURLPOSIXPathStyle); + + /* + * Convert (and copy) the string reference into a Tcl result. + */ + + Tcl_SetObjResult(interp, Tcl_NewStringObj( + CFStringGetCStringPtr(appPath, CFStringGetSystemEncoding()), -1)); + + CFRelease(mainBundleURL); + CFRelease(appPath); + + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * * TkpGetAppName -- * * Retrieves the name of the current application from a platform specific @@ -704,48 +763,37 @@ TkpGetAppName( /* *---------------------------------------------------------------------- * - * TkMacOSXGetAppPathCmd -- + * TkMacOSVersionObjCmd -- * - * Returns the path of the Wish application bundle. + * Tcl command which returns an integer encoding the major and minor + * version numbers of the currently running operating system in the + * form 10000*majorVersion + 100*minorVersion. * * Results: - * Returns the application path. + * Returns the OS version. * * Side effects: * None. * *---------------------------------------------------------------------- */ - + static int -TkMacOSXGetAppPathCmd( - TCL_UNUSED(void *), +TkMacOSVersionObjCmd( + TCL_UNUSED(void *), /* ClientData */ Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) { - if (objc != 1) { + static char version[16] = ""; + if (objc > 1) { Tcl_WrongNumArgs(interp, 1, objv, NULL); return TCL_ERROR; } - - /* - * Get the application path URL and convert it to a string path reference. - */ - - CFURLRef mainBundleURL = CFBundleCopyBundleURL(CFBundleGetMainBundle()); - CFStringRef appPath = - CFURLCopyFileSystemPath(mainBundleURL, kCFURLPOSIXPathStyle); - - /* - * Convert (and copy) the string reference into a Tcl result. - */ - - Tcl_SetObjResult(interp, Tcl_NewStringObj( - CFStringGetCStringPtr(appPath, CFStringGetSystemEncoding()), -1)); - - CFRelease(mainBundleURL); - CFRelease(appPath); + if (version[0] == '\0') { + snprintf(version, 16, "%d", [NSApp macOSVersion]); + } + Tcl_SetResult(interp, version, NULL); return TCL_OK; } @@ -852,10 +900,11 @@ TkMacOSXDefaultStartupScript(void) MODULE_SCOPE void* TkMacOSXGetNamedSymbol( - TCL_UNUSED(const char *), - const char *symbol) + const char* module, + const char* symbol) { void *addr = dlsym(RTLD_NEXT, symbol); + (void)module; if (!addr) { (void) dlerror(); /* Clear dlfcn error state */ diff --git a/macosx/tkMacOSXMenu.c b/macosx/tkMacOSXMenu.c index 1e81fbf..f75db3e 100644 --- a/macosx/tkMacOSXMenu.c +++ b/macosx/tkMacOSXMenu.c @@ -164,7 +164,6 @@ static int ModifierCharWidth(Tk_Font tkfont); TKBackgroundLoop *backgroundLoop = nil; - #pragma mark TKMenu /* diff --git a/macosx/tkMacOSXPrivate.h b/macosx/tkMacOSXPrivate.h index fec267a..71cceb9 100644 --- a/macosx/tkMacOSXPrivate.h +++ b/macosx/tkMacOSXPrivate.h @@ -279,15 +279,19 @@ MODULE_SCOPE void TkMacOSXApplyWindowAttributes(TkWindow *winPtr, NSWindow *macWindow); MODULE_SCOPE Tcl_ObjCmdProc TkMacOSXStandardAboutPanelObjCmd; MODULE_SCOPE Tcl_ObjCmdProc TkMacOSXIconBitmapObjCmd; +MODULE_SCOPE Tcl_ObjCmdProc TkMacOSXNSImageObjCmd; MODULE_SCOPE void TkMacOSXDrawSolidBorder(Tk_Window tkwin, GC gc, int inset, int thickness); MODULE_SCOPE int TkMacOSXServices_Init(Tcl_Interp *interp); MODULE_SCOPE Tcl_ObjCmdProc TkMacOSXRegisterServiceWidgetObjCmd; MODULE_SCOPE unsigned TkMacOSXAddVirtual(unsigned int keycode); +MODULE_SCOPE int TkMacOSXNSImage_Init(Tcl_Interp *interp); MODULE_SCOPE void TkMacOSXWinNSBounds(TkWindow *winPtr, NSView *view, NSRect *bounds); MODULE_SCOPE Bool TkMacOSXInDarkMode(Tk_Window tkwin); MODULE_SCOPE void TkMacOSXDrawAllViews(void *clientData); +MODULE_SCOPE NSColor* controlAccentColor(void); +MODULE_SCOPE void Ttk_MacOSXInit(void); MODULE_SCOPE unsigned long TkMacOSXClearPixel(void); MODULE_SCOPE int MacSystrayInit(Tcl_Interp *); MODULE_SCOPE int MacPrint_Init(Tcl_Interp *); diff --git a/macosx/tkMacOSXRegion.c b/macosx/tkMacOSXRegion.c index b44073a..0537c7b 100644 --- a/macosx/tkMacOSXRegion.c +++ b/macosx/tkMacOSXRegion.c @@ -586,9 +586,9 @@ TkMacOSXCountRectsInRegion( { int rect_count = 0; if (!HIShapeIsEmpty(shape)) { - ChkErr(HIShapeEnumerate, shape, - kHIShapeParseFromBottom|kHIShapeParseFromLeft, - rectCounter, &rect_count); + HIShapeEnumerate(shape, + kHIShapeParseFromBottom|kHIShapeParseFromLeft, + (HIShapeEnumerateProcPtr) rectCounter, (void *) &rect_count); } return rect_count; } @@ -598,9 +598,9 @@ TkMacOSXPrintRectsInRegion( HIShapeRef shape) { if (!HIShapeIsEmpty(shape)) { - ChkErr(HIShapeEnumerate, shape, - kHIShapeParseFromBottom|kHIShapeParseFromLeft, - rectPrinter, NULL); + HIShapeEnumerate( shape, + kHIShapeParseFromBottom|kHIShapeParseFromLeft, + (HIShapeEnumerateProcPtr) rectPrinter, NULL); } } diff --git a/macosx/tkMacOSXSubwindows.c b/macosx/tkMacOSXSubwindows.c index f631b5d..5a16e23 100644 --- a/macosx/tkMacOSXSubwindows.c +++ b/macosx/tkMacOSXSubwindows.c @@ -358,7 +358,7 @@ XUnmapWindow( if (parentPtr && parentPtr->privatePtr->visRgn) { TkMacOSXInvalidateViewRegion( - TkMacOSXGetNSViewForDrawable(parentPtr->privatePtr), + TkMacOSXGetNSViewForDrawable(parentPtr->window), parentPtr->privatePtr->visRgn); } TkMacOSXInvalClipRgns((Tk_Window)parentPtr); diff --git a/macosx/ttkMacOSXTheme.c b/macosx/ttkMacOSXTheme.c index ceeeea4..eb82b88 100644 --- a/macosx/ttkMacOSXTheme.c +++ b/macosx/ttkMacOSXTheme.c @@ -3,12 +3,12 @@ * * Tk theme engine for Mac OSX, using the Appearance Manager API. * - * Copyright © 2004 Joe English - * Copyright © 2005 Neil Madden - * Copyright © 2006-2009 Daniel A. Steffen <das@users.sourceforge.net> - * Copyright © 2008-2009 Apple Inc. - * Copyright © 2009 Kevin Walzer/WordTech Communications LLC. - * Copyright © 2019 Marc Culler + * Copyright (c) 2004 Joe English + * Copyright (c) 2005 Neil Madden + * Copyright (c) 2006-2009 Daniel A. Steffen <das@users.sourceforge.net> + * Copyright (c) 2008-2009 Apple Inc. + * Copyright (c) 2009 Kevin Walzer/WordTech Communications LLC. + * Copyright (c) 2019 Marc Culler * * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. @@ -31,133 +31,157 @@ #include "tkMacOSXPrivate.h" #include "ttk/ttkTheme.h" +#include "ttkMacOSXTheme.h" +#include "tkColor.h" +#include <math.h> + +MODULE_SCOPE NSColor *controlAccentColor(void) { + static int accentPixel = -1; + if (accentPixel == -1) { + TkColor *temp = TkpGetColor(NULL, "systemControlAccentColor"); + accentPixel = temp->color.pixel; + ckfree(temp); + } + return TkMacOSXGetNSColor(NULL, accentPixel); +} /* - * Macros for handling drawing contexts. + * Values which depend on the OS version. These are initialized + * in Ttk_MacOSXInit. */ -#define BEGIN_DRAWING(d) { \ - TkMacOSXDrawingContext dc; \ - if (!TkMacOSXSetupDrawingContext((d), NULL, &dc)) { \ - return; \ - } \ +static Ttk_Padding entryElementPadding; +static CGFloat Ttk_ContrastDelta; -#define END_DRAWING \ - TkMacOSXRestoreDrawingContext(&dc);} +/*---------------------------------------------------------------------- + * +++ ComputeButtonDrawInfo -- + * + * Fill in an appearance manager HIThemeButtonDrawInfo record + * from a Ttk state and the ThemeButtonParams used as the + * clientData. + */ -#define HIOrientation kHIThemeOrientationNormal -#define NoThemeMetric 0xFFFFFFFF +static inline HIThemeButtonDrawInfo ComputeButtonDrawInfo( + ThemeButtonParams *params, + Ttk_State state, + TCL_UNUSED(Tk_Window)) +{ + /* + * See ButtonElementDraw for the explanation of why we always draw + * some buttons in the active state. + */ -#ifdef __LP64__ -#define RangeToFactor(maximum) (((double) (INT_MAX >> 1)) / (maximum)) -#else -#define RangeToFactor(maximum) (((double) (LONG_MAX >> 1)) / (maximum)) -#endif /* __LP64__ */ + SInt32 HIThemeState; + int adornment = 0; -#define TTK_STATE_FIRST_TAB TTK_STATE_USER1 -#define TTK_STATE_LAST_TAB TTK_STATE_USER2 -#define TTK_TREEVIEW_STATE_SORTARROW TTK_STATE_USER1 + HIThemeState = Ttk_StateTableLookup(ThemeStateTable, state); -/* - * Colors and gradients used in Dark Mode. - */ + /* + * HITheme uses the adornment to decide the direction of the + * arrow on a Disclosure Button. Also HITheme draws inactive + * (TTK_STATE_BACKGROUND) buttons in a gray color but macOS + * no longer does that. So we adjust the HIThemeState. + */ -static CGFloat darkButtonFace[4] = { - 90.0 / 255, 86.0 / 255, 95.0 / 255, 1.0 -}; -static CGFloat darkPressedButtonFace[4] = { - 114.0 / 255, 110.0 / 255, 118.0 / 255, 1.0 -}; -static CGFloat darkPressedBevelFace[4] = { - 135.0 / 255, 136.0 / 255, 138.0 / 255, 1.0 -}; -static CGFloat darkSelectedBevelFace[4] = { - 162.0 / 255, 163.0 / 255, 165.0 / 255, 1.0 -}; -static CGFloat darkDisabledButtonFace[4] = { - 86.0 / 255, 87.0 / 255, 89.0 / 255, 1.0 -}; -static CGFloat darkInactiveSelectedTab[4] = { - 159.0 / 255, 160.0 / 255, 161.0 / 255, 1.0 -}; -static CGFloat darkSelectedTab[4] = { - 97.0 / 255, 94.0 / 255, 102.0 / 255, 1.0 -}; -static CGFloat darkTab[4] = { - 44.0 / 255, 41.0 / 255, 50.0 / 255, 1.0 -}; -static CGFloat darkFocusRing[4] = { - 38.0 / 255, 113.0 / 255, 159.0 / 255, 1.0 -}; -static CGFloat darkFocusRingTop[4] = { - 50.0 / 255, 124.0 / 255, 171.0 / 255, 1.0 -}; -static CGFloat darkFocusRingBottom[4] = { - 57.0 / 255, 130.0 / 255, 176.0 / 255, 1.0 -}; -static CGFloat darkTabSeparator[4] = {0.0, 0.0, 0.0, 0.25}; -static CGFloat darkTrack[4] = {1.0, 1.0, 1.0, 0.25}; -static CGFloat darkFrameTop[4] = {1.0, 1.0, 1.0, 0.0625}; -static CGFloat darkFrameBottom[4] = {1.0, 1.0, 1.0, 0.125}; -static CGFloat darkFrameAccent[4] = {0.0, 0.0, 0.0, 0.0625}; -static CGFloat darkTopGradient[8] = { - 1.0, 1.0, 1.0, 0.3, - 1.0, 1.0, 1.0, 0.0 -}; -static CGFloat darkBackgroundGradient[8] = { - 0.0, 0.0, 0.0, 0.1, - 0.0, 0.0, 0.0, 0.25 -}; -static CGFloat darkInactiveGradient[8] = { - 89.0 / 255, 90.0 / 255, 93.0 / 255, 1.0, - 119.0 / 255, 120.0 / 255, 122.0 / 255, 1.0 -}; -static CGFloat darkSelectedGradient[8] = { - 23.0 / 255, 111.0 / 255, 232.0 / 255, 1.0, - 20.0 / 255, 94.0 / 255, 206.0 / 255, 1.0 -}; -static CGFloat pressedPushButtonGradient[8] = { - 35.0 / 255, 123.0 / 255, 244.0 / 255, 1.0, - 30.0 / 255, 114.0 / 255, 235.0 / 255, 1.0 -}; + switch (params->kind) { + case kThemeArrowButton: + adornment = kThemeAdornmentDrawIndicatorOnly; + if (state & TTK_STATE_SELECTED) { + adornment |= kThemeAdornmentArrowUpArrow; + } + /* Fall through. */ + case kThemeRadioButton: + /* + * The gray color is better than the blue color for a + * background selected Radio Button. + */ + + if (state & TTK_STATE_SELECTED) { + break; + } + default: + if (state & TTK_STATE_BACKGROUND) { + HIThemeState |= kThemeStateActive; + } + break; + } + + const HIThemeButtonDrawInfo info = { + .version = 0, + .state = HIThemeState, + .kind = params ? params->kind : 0, + .value = Ttk_StateTableLookup(ButtonValueTable, state), + .adornment = Ttk_StateTableLookup(ButtonAdornmentTable, state) | adornment, + }; + return info; +} /* - * When building on systems earlier than 10.8 there is no reasonable way to - * convert an NSColor to a CGColor. We do run-time checking of the OS version, - * and never need the CGColor property on older systems, so we can use this - * CGCOLOR macro, which evaluates to NULL without raising compiler warnings. - * Similarly, we never draw rounded rectangles on older systems which did not - * have CGPathCreateWithRoundedRect, so we just redefine it to return NULL. + * When we draw simulated Apple widgets we use the Core Graphics framework. + * Core Graphics uses CGColorRefs, not NSColors. A CGColorRef must be retained + * and released explicitly while an NSColor is autoreleased. In version 10.8 + * of macOS Apple introduced a CGColor property of an NSColor which is guaranteed + * to be a valid CGColorRef for (approximately) the same color and is released + * when the NSColor is autoreleased. + * + * When building on systems earlier than 10.8 there is no painless way to + * convert an NSColor to a CGColor. On the other hand, on those systems we use + * the HIToolbox to draw all widgets, so we never need to call Core Graphics + * drawing routines directly. This means that the functions and macros below + * which construct CGColorRefs can be defined to return nil on systems before + * 10.8. + * + * Similarly, those older systems did not have CGPathCreateWithRoundedRect, but + * since we never need to draw rounded rectangles on those systems we can just + * define it to return nil. */ #if MAC_OS_X_VERSION_MAX_ALLOWED >= 1080 +static CGColorRef +CGColorFromRGBA( + CGFloat *rgba) +{ + NSColorSpace *colorSpace = [NSColorSpace sRGBColorSpace]; + NSColor *nscolor = [NSColor colorWithColorSpace: colorSpace + components: rgba + count: 4]; + return nscolor.CGColor; +} + +static CGColorRef +CGColorFromGray( + GrayColor g) +{ + CGFloat rgba[4] = {g.grayscale, g.grayscale, g.grayscale, g.alpha}; + NSColorSpace *colorSpace = [NSColorSpace sRGBColorSpace]; + NSColor *nscolor = [NSColor colorWithColorSpace: colorSpace + components: rgba + count: 4]; + return nscolor.CGColor; +} + #define CGCOLOR(nscolor) (nscolor).CGColor + #else -#define CGCOLOR(nscolor) (0 ? (CGColorRef) (nscolor) : NULL) -#define CGPathCreateWithRoundedRect(w, x, y, z) NULL -#endif -/* - * If we try to draw a rounded rectangle with too large of a radius - * CoreGraphics will raise a fatal exception. This macro returns if - * the width or height is less than twice the radius. Presumably this - * only happens when a widget has not yet been configured and has size - * 1x1. - */ +#define CGCOLOR(nscolor) NULL +#define CGColorFromRGBA(rgba) NULL +#define CGColorFromGray(gray) NULL +#define CGPathCreateWithRoundedRect(w, x, y, z) NULL -#define CHECK_RADIUS(radius, bounds) \ - if ((radius) > (bounds).size.width / 2 || (radius) > (bounds).size.height / 2) { \ - return; \ - } +#endif /*---------------------------------------------------------------------- * +++ Utilities. */ -/* +/*---------------------------------------------------------------------- * BoxToRect -- - * Convert a Ttk_Box in Tk coordinates relative to the given Drawable - * to a native Rect relative to the containing port. + * + * Convert a Ttk_Box in Tk coordinates relative to the given Drawable to a + * native CGRect relative to the containing NSView. (The coordinate system + * is the one used by CGContextRef, which has origin at the upper left + * corner, and y increasing downward.) */ static inline CGRect BoxToRect( @@ -175,60 +199,85 @@ static inline CGRect BoxToRect( return rect; } -/* - * Table mapping Tk states to Appearance manager ThemeStates +/*---------------------------------------------------------------------- + * LookupGrayPalette + * + * Retrieves the palette of grayscale colors needed to draw a particular + * type of button, in a particular state, in light or dark mode. + * */ -static Ttk_StateTable ThemeStateTable[] = { - {kThemeStateActive, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, - {kThemeStateUnavailable, TTK_STATE_DISABLED, 0}, - {kThemeStatePressed, TTK_STATE_PRESSED, 0}, - {kThemeStateInactive, TTK_STATE_BACKGROUND, 0}, - {kThemeStateActive, 0, 0} - - /* Others: Not sure what these are supposed to mean. Up/Down have - * something to do with "little arrow" increment controls... Dunno what - * a "Rollover" is. - * NEM: Rollover is TTK_STATE_ACTIVE... but we don't handle that yet, by - * the looks of things - * - * {kThemeStateRollover, 0, 0}, - * {kThemeStateUnavailableInactive, 0, 0} - * {kThemeStatePressedUp, 0, 0}, - * {kThemeStatePressedDown, 0, 0} - */ -}; +static GrayPalette LookupGrayPalette( + const ButtonDesign *design, + unsigned int state, + int isDark) +{ + const PaletteStateTable *entry = design->palettes; + while ((state & entry->onBits) != entry->onBits || + (~state & entry->offBits) != entry->offBits) + { + ++entry; + } + return isDark ? entry->dark : entry->light; +} /*---------------------------------------------------------------------- * NormalizeButtonBounds -- * - * Apple's Human Interface Guidelines only allow three specific heights - * for most buttons: Regular, small and mini. We always use the regular - * size. However, Ttk may provide an arbitrary bounding rectangle. We - * always draw the button centered vertically on the rectangle, and - * having the same width as the rectangle. This function returns the - * actual bounding rectangle that will be used in drawing the button. + * This function returns the actual bounding rectangle that will be used + * in drawing the button. + * + * Apple only allows three specific heights for most buttons: regular, + * small and mini. We always use the regular size. However, Ttk may + * provide a bounding rectangle with arbitrary height. We draw the Mac + * button centered vertically in the Ttk rectangle, with the same width as + * the rectangle. (But we take care to produce an integer y coordinate, + * to avoid unexpected anti-aliasing.) + * + * In addition, the button types which are not known to HIToolbox need some + * adjustments to their bounds. * - * The BevelButton is allowed to have arbitrary size, and also has - * external padding. This is handled separately here. */ static CGRect NormalizeButtonBounds( - SInt32 heightMetric, - CGRect bounds) + ThemeButtonParams *params, + CGRect bounds, + int isDark) { SInt32 height; - if (heightMetric != (SInt32) NoThemeMetric) { - ChkErr(GetThemeMetric, heightMetric, &height); - bounds.origin.y += (bounds.size.height - height) / 2; + if (params->heightMetric != NoThemeMetric) { + ChkErr(GetThemeMetric, params->heightMetric, &height); + height += 2; + bounds.origin.y += round(1 + (bounds.size.height - height) / 2); bounds.size.height = height; } + switch (params->kind) { + case TkRoundedRectButton: + bounds.size.height -= 1; + break; + case TkInlineButton: + bounds.size.height -= 4; + bounds.origin.y += 1; + break; + case TkRecessedButton: + bounds.size.height -= 2; + break; + case kThemeRoundButtonHelp: + if (isDark) { + bounds.size.height = bounds.size.width = 22; + } else { + bounds.size.height = bounds.size.width = 22; + } + break; + default: + break; + } return bounds; } /*---------------------------------------------------------------------- - * +++ Backgrounds + * +++ Background Colors * * Support for contrasting background colors when GroupBoxes or Tabbed * panes are nested inside each other. Early versions of macOS used ridged @@ -243,31 +292,31 @@ static CGRect NormalizeButtonBounds( * support Dark Mode anyway. */ -static const CGFloat WINDOWBACKGROUND[4] = { - 235.0 / 255, 235.0 / 255, 235.0 / 255, 1.0 -}; -static const CGFloat WHITERGBA[4] = {1.0, 1.0, 1.0, 1.0}; -static const CGFloat BLACKRGBA[4] = {0.0, 0.0, 0.0, 1.0}; +RGBACOLOR windowBackground[4] = RGBA256(235.0, 235.0, 235.0, 1.0); /*---------------------------------------------------------------------- * GetBackgroundColor -- * * Fills the array rgba with the color coordinates for a background color. - * Start with the background color of a window's geometry container, or - * the standard ttk window background if there is no container. If the + * Start with the background color of a window's container, or the + * standard ttk window background if there is no container. If the * contrast parameter is nonzero, modify this color to be darker, for the * aqua appearance, or lighter for the DarkAqua appearance. This is - * primarily used by the Fill and Background elements. + * primarily used by the Fill and Background elements. The save parameter + * is normally YES, so the contrasting color is saved in the private + * data of the widget. This behavior can be disabled in special cases, + * such as when drawing notebook tabs in macOS 11. */ -static void GetBackgroundColor( +static void GetBackgroundColorRGBA( TCL_UNUSED(CGContextRef), Tk_Window tkwin, int contrast, + Bool save, CGFloat *rgba) { - TkWindow *winPtr = (TkWindow *)tkwin; - TkWindow *containerPtr = (TkWindow *)TkGetContainer(tkwin); + TkWindow *winPtr = (TkWindow *) tkwin; + TkWindow *containerPtr = (TkWindow *) TkGetContainer(tkwin); while (containerPtr && containerPtr->privatePtr) { if (containerPtr->privatePtr->flags & TTK_HAS_CONTRASTING_BG) { @@ -287,23 +336,24 @@ static void GetBackgroundColor( [windowColor getComponents: rgba]; } else { for (int i = 0; i < 4; i++) { - rgba[i] = WINDOWBACKGROUND[i]; + rgba[i] = windowBackground[i]; } } } + if (contrast) { int isDark = (rgba[0] + rgba[1] + rgba[2] < 1.5); if (isDark) { for (int i = 0; i < 3; i++) { - rgba[i] += 8.0 / 255.0; + rgba[i] += Ttk_ContrastDelta*contrast / 255.0; } } else { for (int i = 0; i < 3; i++) { - rgba[i] -= 8.0 / 255.0; + rgba[i] -= Ttk_ContrastDelta*contrast / 255.0; } } - if (winPtr->privatePtr) { + if (save && winPtr->privatePtr) { winPtr->privatePtr->flags |= TTK_HAS_CONTRASTING_BG; for (int i = 0; i < 4; i++) { winPtr->privatePtr->fillRGBA[i] = rgba[i]; @@ -312,12 +362,313 @@ static void GetBackgroundColor( } } +static CGColorRef GetBackgroundCGColor( + CGContextRef context, + Tk_Window tkwin, + int contrast, + Bool save) +{ + CGFloat rgba[4]; + GetBackgroundColorRGBA(context, tkwin, contrast, save, rgba); + return CGColorFromRGBA(rgba); +} + +/*---------------------------------------------------------------------- + * +++ Buttons + */ + +/*---------------------------------------------------------------------- + * FillRoundedRectangle -- + * + * Fill a rounded rectangle with a specified solid color. + */ + +static void FillRoundedRectangle( + CGContextRef context, + CGRect bounds, + CGFloat radius, + CGColorRef color) +{ + CGPathRef path; + CHECK_RADIUS(radius, bounds) + + path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); + if (!path) { + return; + } + CGContextSetFillColorWithColor(context, color); + CGContextBeginPath(context); + CGContextAddPath(context, path); + CGContextFillPath(context); + CFRelease(path); +} + +/*---------------------------------------------------------------------- + * FillBorder -- + * + * Draw a 1-pixel border around a rounded rectangle using a 3-step + * gradient of shades of gray. + */ + +static void FillBorder( + CGContextRef context, + CGRect bounds, + GrayPalette palette, + CGFloat radius) +{ + if (bounds.size.width < 2) { + return; + } + NSColorSpace *sRGB = [NSColorSpace sRGBColorSpace]; + CGPoint end = CGPointMake(bounds.origin.x, bounds.origin.y + bounds.size.height); + CGFloat corner = (radius > 0 ? radius : 2.0) / bounds.size.height; + CGFloat locations[4] = {0.0, corner, 1.0 - corner, 1.0}; + CGPathRef path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); + CGFloat colors[16]; + colors[0] = colors[1] = colors[2] = palette.top / 255.0; + colors[4] = colors[5] = colors[6] = palette.side / 255.0; + colors[8] = colors[9] = colors[10] = palette.side / 255.0; + colors[12] = colors[13] = colors[14] = palette.bottom / 255.0; + colors[3] = colors[7] = colors[11] = colors[15] = 1.0; + CGGradientRef gradient = CGGradientCreateWithColorComponents( + sRGB.CGColorSpace, colors, locations, 4); + if (!gradient) { + return; + } + CGContextSaveGState(context); + CGContextBeginPath(context); + CGContextAddPath(context, path); + CGContextClip(context); + CGContextDrawLinearGradient(context, gradient, bounds.origin, end, 0.0); + CGContextRestoreGState(context); + CFRelease(path); + CFRelease(gradient); +} + +/*---------------------------------------------------------------------- + * DrawFocusRing -- + * + * Draw a 4-pixel wide rounded focus ring enclosing a rounded + * rectangle, using the current system accent color. + */ + +static void DrawFocusRing( + CGContextRef context, + CGRect bounds, + const ButtonDesign *design) +{ + CGColorRef highlightColor; + CGFloat highlight[4] = {1.0, 1.0, 1.0, 0.2}; + CGColorRef focusColor; + + focusColor = CGCOLOR([controlAccentColor() colorWithAlphaComponent:0.6]); + FillRoundedRectangle(context, bounds, design->radius, focusColor); + bounds = CGRectInset(bounds, 3, 3); + highlightColor = CGColorFromRGBA(highlight); + CGContextSetFillColorWithColor(context, highlightColor); + CGContextFillRect(context, bounds); +} + +/*---------------------------------------------------------------------- + * DrawGrayButton -- + * + * Draw a button in normal gray colors. + * + * Aqua buttons are normally drawn in a grayscale color. The buttons, + * which are shaped as rounded rectangles have a 1-pixel border which is + * drawn in a 3-step gradient and a solid gray face. + * + * Note that this will produce a round button if length = width = + * 2*radius. + */ + +static void DrawGrayButton( + CGContextRef context, + CGRect bounds, + const ButtonDesign *design, + unsigned int state, + Tk_Window tkwin) +{ + int isDark = TkMacOSXInDarkMode(tkwin); + GrayPalette palette = LookupGrayPalette(design, state, isDark); + GrayColor faceGray = {.grayscale = 0.0, .alpha = 1.0}; + CGFloat radius = 2 * design->radius <= bounds.size.height ? + design->radius : bounds.size.height / 2; + if (palette.top <= 255.0) { + FillBorder(context, bounds, palette, radius); + } + if (palette.face <= 255.0) { + faceGray.grayscale = palette.face / 255.0; + } else { + + /* + * Color values > 255 are "transparent" which really means that we + * fill with the background color. + */ + + CGFloat rgba[4], gray; + GetBackgroundColorRGBA(context, tkwin, 0, NO, rgba); + gray = (rgba[0] + rgba[1] + rgba[2]) / 3.0; + faceGray.grayscale = gray; + } + FillRoundedRectangle(context, CGRectInset(bounds, 1, 1), radius - 1, + CGColorFromGray(faceGray)); +} + +/*---------------------------------------------------------------------- + * DrawAccentedButton -- + * + * The accent color is only used when drawing buttons in the active + * window. Push Buttons and segmented Arrow Buttons are drawn in color + * when in the pressed state. Selected Check Buttons, Radio Buttons and + * notebook Tabs are also drawn in color. The color is based on the + * user's current choice for the controlAccentColor, but is actually a + * linear gradient with a 1-pixel darker line at the top and otherwise + * changing from lighter at the top to darker at the bottom. This + * function draws a colored rounded rectangular button. + */ + +static void DrawAccentedButton( + CGContextRef context, + CGRect bounds, + const ButtonDesign *design, + int state, + int isDark) +{ + NSColorSpace *sRGB = [NSColorSpace sRGBColorSpace]; + CGColorRef faceColor = CGCOLOR(controlAccentColor()); + CGFloat radius = design->radius; + CGPathRef path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); + // This gradient should only be used for PushButtons and Tabs, and it needs + // to be lighter at the top. + static CGFloat components[12] = {1.0, 1.0, 1.0, 0.05, + 1.0, 1.0, 1.0, 0.2, + 1.0, 1.0, 1.0, 0.0}; + CGFloat locations[3] = {0.0, 0.05, 1.0}; + CGGradientRef gradient = CGGradientCreateWithColorComponents( + sRGB.CGColorSpace, components, locations, 3); + CGPoint end; + if (bounds.size.height > 2*radius) { + bounds.size.height -= 1; + } + end = CGPointMake(bounds.origin.x, bounds.origin.y + bounds.size.height); + CGContextSaveGState(context); + CGContextBeginPath(context); + CGContextAddPath(context, path); + CGContextClip(context); + FillRoundedRectangle(context, bounds, radius, faceColor); + CGContextDrawLinearGradient(context, gradient, bounds.origin, end, 0.0); + if (state & TTK_STATE_PRESSED && + state & TTK_STATE_ALTERNATE) { + CGColorRef color = isDark ? + CGColorFromGray(darkPressedDefaultButton) : + CGColorFromGray(pressedDefaultButton); + FillRoundedRectangle(context, bounds, radius, color); + } + CGContextRestoreGState(context); + CFRelease(path); + CFRelease(gradient); +} /*---------------------------------------------------------------------- - * +++ Single Arrow Images -- + * DrawAccentedSegment -- * - * Used in ListHeaders and Comboboxes as well as disclosure triangles in - * macOS 11. + * Draw the colored ends of widgets like popup buttons and combo buttons. + */ + +static void DrawAccentedSegment( + CGContextRef context, + CGRect bounds, + const ButtonDesign *design, + unsigned int state, + Tk_Window tkwin) +{ + /* + * Clip to the bounds and then draw an accented button which is extended so + * that the rounded corners on the left will be clipped off. This assumes + * that the bounds include room for the focus ring. + */ + int isDark = TkMacOSXInDarkMode(tkwin); + GrayColor sepGray = isDark ? darkComboSeparator : lightComboSeparator; + CGColorRef sepColor = CGColorFromGray(sepGray); + CGRect clip = bounds; + clip.size.height += 10; + bounds.origin.x -= 10; + bounds.size.width += 10; + CGPoint separator[2] = { + CGPointMake(clip.origin.x - 1, bounds.origin.y + 5), + CGPointMake(clip.origin.x - 1, + bounds.origin.y + bounds.size.height - 3)}; + CGContextSaveGState(context); + CGContextSetStrokeColorWithColor(context, sepColor); + CGContextSetShouldAntialias(context, false); + CGContextSetLineWidth(context, 0.5); + CGContextAddLines(context, separator, 2); + CGContextStrokePath(context); + CGContextSetShouldAntialias(context, true); + if (state & TTK_STATE_FOCUS) { + CGRect focusClip = clip; + clip.size.width += 4; + CGContextClipToRect(context, focusClip); + bounds = CGRectInset(bounds, 0, 1); + DrawFocusRing(context, bounds, design); + } + bounds = CGRectInset(bounds, 4, 4); + if (state & TTK_STATE_BACKGROUND) { + bounds.size.height += 2; + } else { + bounds.size.height += 1; + } + CGContextClipToRect(context, clip); + if ((state & TTK_STATE_BACKGROUND) || (state & TTK_STATE_DISABLED)) { + DrawGrayButton(context, bounds, design, state, tkwin); + } else { + DrawAccentedButton(context, bounds, design, state | TTK_STATE_ALTERNATE, + isDark); + } + CGContextRestoreGState(context); +} + +/*---------------------------------------------------------------------- + * +++ Entry boxes + */ + +static void DrawEntry( + CGContextRef context, + CGRect bounds, + const ButtonDesign *design, + int state, + Tk_Window tkwin) +{ + int isDark = TkMacOSXInDarkMode(tkwin); + GrayPalette palette = LookupGrayPalette(design, state, isDark); + CGColorRef backgroundColor; + CGFloat bgRGBA[4]; + if (isDark) { + GetBackgroundColorRGBA(context, tkwin, 0, NO, bgRGBA); + + /* + * Lighten the entry background to provide contrast. + */ + + for (int i = 0; i < 3; i++) { + bgRGBA[i] += 8.0 / 255.0; + } + backgroundColor = CGColorFromRGBA(bgRGBA); + } else { + backgroundColor = CG_WHITE; + } + if (state & TTK_STATE_FOCUS) { + DrawFocusRing(context, bounds, design); + } else { + FillBorder(context, CGRectInset(bounds,3,3), palette, design->radius); + } + bounds = CGRectInset(bounds, 4, 4); + FillRoundedRectangle(context, bounds, design->radius, backgroundColor); +} + +/*---------------------------------------------------------------------- + * +++ Chevrons, CheckMarks, etc. -- */ static void DrawDownArrow( @@ -325,14 +676,23 @@ static void DrawDownArrow( CGRect bounds, CGFloat inset, CGFloat size, - const CGFloat *rgba) + int state) { + CGColorRef strokeColor; CGFloat x, y; - CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); + + if (state & TTK_STATE_DISABLED) { + strokeColor = CGCOLOR([NSColor disabledControlTextColor]); + } else if (state & TTK_STATE_IS_ACCENTED) { + strokeColor = CG_WHITE; + } else { + strokeColor = CGCOLOR([NSColor controlTextColor]); + } + CGContextSetStrokeColorWithColor(context, strokeColor); CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; - y = bounds.origin.y + trunc(bounds.size.height / 2); + y = bounds.origin.y + trunc(bounds.size.height / 2) + 1; CGContextBeginPath(context); CGPoint arrow[3] = { {x, y - size / 4}, {x + size / 2, y + size / 4}, @@ -342,16 +702,28 @@ static void DrawDownArrow( CGContextStrokePath(context); } +/*---------------------------------------------------------------------- + * DrawUpArrow -- + * + * Draws a single upward pointing arrow for ListHeaders and Disclosure Buttons. + */ + static void DrawUpArrow( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, - const CGFloat *rgba) + int state) { + NSColor *strokeColor; CGFloat x, y; - CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); + if (state & TTK_STATE_DISABLED) { + strokeColor = [NSColor disabledControlTextColor]; + } else { + strokeColor = [NSColor controlTextColor]; + } + CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2); @@ -364,28 +736,60 @@ static void DrawUpArrow( CGContextStrokePath(context); } -static void DrawClosedDisclosure( +/*---------------------------------------------------------------------- + * DrawUpDownArrows -- + * + * Draws the double arrows used in menu buttons and spin buttons. + */ + +static void DrawUpDownArrows( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, - CGFloat *rgba) + CGFloat gap, + int state, + ThemeDrawState drawState) { CGFloat x, y; - - CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); + NSColor *topStrokeColor, *bottomStrokeColor; + if (drawState == BOTH_ARROWS && !(state & TTK_STATE_BACKGROUND)) { + topStrokeColor = bottomStrokeColor = [NSColor whiteColor]; + } else if (drawState == kThemeStatePressedDown) { + topStrokeColor = [NSColor controlTextColor]; + bottomStrokeColor = [NSColor whiteColor]; + } else if (drawState == kThemeStatePressedUp) { + topStrokeColor = [NSColor whiteColor]; + bottomStrokeColor = [NSColor controlTextColor]; + } else if (state & TTK_STATE_DISABLED) { + topStrokeColor = bottomStrokeColor = [NSColor disabledControlTextColor]; + } else { + topStrokeColor = bottomStrokeColor = [NSColor controlTextColor]; + } CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2); CGContextBeginPath(context); - CGPoint arrow[3] = { - {x, y - size / 4 - 1}, {x + size / 2, y}, {x, y + size / 4 + 1} - }; - CGContextAddLines(context, arrow, 3); + CGPoint bottomArrow[3] = + {{x, y + gap}, {x + size / 2, y + gap + size / 2}, {x + size, y + gap}}; + CGContextAddLines(context, bottomArrow, 3); + CGContextSetStrokeColorWithColor(context, CGCOLOR(bottomStrokeColor)); + CGContextStrokePath(context); + CGContextBeginPath(context); + CGPoint topArrow[3] = + {{x, y - gap}, {x + size / 2, y - gap - size / 2}, {x + size, y - gap}}; + CGContextAddLines(context, topArrow, 3); + CGContextSetStrokeColorWithColor(context, CGCOLOR(topStrokeColor)); CGContextStrokePath(context); } -static void DrawOpenDisclosure( +/*---------------------------------------------------------------------- + * DrawClosedDisclosure -- + * + * Draws a disclosure chevron in the Big Sur style, for Treeviews. + */ + +static void DrawClosedDisclosure( CGContextRef context, CGRect bounds, CGFloat inset, @@ -400,24 +804,24 @@ static void DrawOpenDisclosure( y = bounds.origin.y + trunc(bounds.size.height / 2); CGContextBeginPath(context); CGPoint arrow[3] = { - {x, y - size / 4}, {x + size / 2, y + size / 2}, {x + size, y - size / 4} + {x, y - size / 4 - 1}, {x + size / 2, y}, {x, y + size / 4 + 1} }; CGContextAddLines(context, arrow, 3); CGContextStrokePath(context); } /*---------------------------------------------------------------------- - * +++ Double Arrow Buttons -- + * DrawOpenDisclosure -- * - * Used in MenuButtons and SpinButtons. + * Draws an open disclosure chevron in the Big Sur style, for Treeviews. */ -static void DrawUpDownArrows( +static void DrawOpenDisclosure( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, - const CGFloat *rgba) + CGFloat *rgba) { CGFloat x, y; @@ -426,602 +830,657 @@ static void DrawUpDownArrows( x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2); CGContextBeginPath(context); - CGPoint bottomArrow[3] = - {{x, y + 2}, {x + size / 2, y + 2 + size / 2}, {x + size, y + 2}}; - CGContextAddLines(context, bottomArrow, 3); - CGPoint topArrow[3] = - {{x, y - 2}, {x + size / 2, y - 2 - size / 2}, {x + size, y - 2}}; - CGContextAddLines(context, topArrow, 3); + CGPoint arrow[3] = { + {x, y - size / 4}, {x + size / 2, y + size / 2}, {x + size, y - size / 4} + }; + CGContextAddLines(context, arrow, 3); CGContextStrokePath(context); } - /*---------------------------------------------------------------------- - * +++ FillButtonBackground -- + * IndicatorColor -- * - * Fills a rounded rectangle with a transparent black gradient. - * This is a no-op if building on 10.8 or older. + * Returns a CGColorRef of the appropriate shade for a check button or + * radio button in a given state. */ -static void FillButtonBackground( - CGContextRef context, - CGRect bounds, - CGFloat radius) +static CGColorRef IndicatorColor( + int state, + int isDark) { - CHECK_RADIUS(radius, bounds) - - CGPathRef path; - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - CGGradientRef backgroundGradient = CGGradientCreateWithColorComponents( - deviceRGB.CGColorSpace, darkBackgroundGradient, NULL, 2); - CGPoint backgroundEnd = { - bounds.origin.x, - bounds.origin.y + bounds.size.height - }; - CGContextBeginPath(context); - path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); - CGContextAddPath(context, path); - CGContextClip(context); - CGContextDrawLinearGradient(context, backgroundGradient, - bounds.origin, backgroundEnd, 0); - CFRelease(path); - CFRelease(backgroundGradient); + if (state & TTK_STATE_DISABLED) { + return isDark ? + CGColorFromGray(darkDisabledIndicator) : + CGColorFromGray(lightDisabledIndicator); + } else if ((state & TTK_STATE_SELECTED || state & TTK_STATE_ALTERNATE) && + !(state & TTK_STATE_BACKGROUND)) { + return CG_WHITE; + } else { + return CGCOLOR([NSColor controlTextColor]); + } } /*---------------------------------------------------------------------- - * +++ HighlightButtonBorder -- + * DrawCheckIndicator -- * - * Accent the top border of a rounded rectangle with a transparent - * white gradient. + * Draws the checkmark or horizontal bar in a check box. */ -static void HighlightButtonBorder( +static void DrawCheckIndicator( CGContextRef context, - CGRect bounds) -{ - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - CGPoint topEnd = {bounds.origin.x, bounds.origin.y + 3}; - CGGradientRef topGradient = CGGradientCreateWithColorComponents( - deviceRGB.CGColorSpace, darkTopGradient, NULL, 2); - - CGContextSaveGState(context); - CGContextBeginPath(context); - CGContextAddArc(context, bounds.origin.x + 4, bounds.origin.y + 4, - 4, PI, 3 * PI / 2, 0); - CGContextAddArc(context, bounds.origin.x + bounds.size.width - 4, - bounds.origin.y + 4, 4, 3 * PI / 2, 0, 0); - CGContextReplacePathWithStrokedPath(context); - CGContextClip(context); - CGContextDrawLinearGradient(context, topGradient, bounds.origin, topEnd, - 0.0); - CGContextRestoreGState(context); - CFRelease(topGradient); -} - -/*---------------------------------------------------------------------- - * DrawGroupBox -- - * - * This is a standalone drawing procedure which draws the contrasting - * rounded rectangular box for LabelFrames and Notebook panes used in - * more recent versions of macOS. - */ - -static void DrawGroupBox( CGRect bounds, - CGContextRef context, - Tk_Window tkwin) + int state, + int isDark) { - CHECK_RADIUS(4, bounds) - - CGPathRef path; - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *borderColor, *bgColor; - static CGFloat border[4] = {1.0, 1.0, 1.0, 0.25}; - CGFloat fill[4]; - - GetBackgroundColor(context, tkwin, 1, fill); - bgColor = [NSColor colorWithColorSpace: deviceRGB components: fill - count: 4]; - CGContextSetFillColorSpace(context, deviceRGB.CGColorSpace); - CGContextSetFillColorWithColor(context, CGCOLOR(bgColor)); - path = CGPathCreateWithRoundedRect(bounds, 4, 4, NULL); - CGContextClipToRect(context, bounds); - CGContextBeginPath(context); - CGContextAddPath(context, path); - CGContextFillPath(context); - borderColor = [NSColor colorWithColorSpace: deviceRGB components: border - count: 4]; - CGContextSetFillColorWithColor(context, CGCOLOR(borderColor)); - [borderColor getComponents: fill]; - CGContextSetRGBFillColor(context, fill[0], fill[1], fill[2], fill[3]); + CGFloat x = bounds.origin.x, y = bounds.origin.y; + CGColorRef strokeColor = IndicatorColor(state, isDark); - CGContextBeginPath(context); - CGContextAddPath(context, path); - CGContextReplacePathWithStrokedPath(context); - CGContextFillPath(context); - CFRelease(path); + CGContextSetStrokeColorWithColor(context, strokeColor); + if (state & TTK_STATE_SELECTED) { + CGContextSetLineWidth(context, 1.5); + CGContextBeginPath(context); + CGPoint check[3] = {{x + 3, y + 7}, {x + 6, y + 10}, {x + 10, y + 3}}; + CGContextAddLines(context, check, 3); + CGContextStrokePath(context); + } else if (state & TTK_STATE_ALTERNATE) { + CGContextSetLineWidth(context, 2.0); + CGContextBeginPath(context); + CGPoint bar[2] = {{x + 3, y + 7}, {x + 11, y + 7}}; + CGContextAddLines(context, bar, 2); + CGContextStrokePath(context); + } } /*---------------------------------------------------------------------- - * SolidFillRoundedRectangle -- + * DrawRadioIndicator -- * - * Fill a rounded rectangle with a specified solid color. + * Draws the dot in the middle of a selected radio button. */ -static void SolidFillRoundedRectangle( +static void DrawRadioIndicator( CGContextRef context, CGRect bounds, - CGFloat radius, - NSColor *color) + int state, + int isDark) { - CGPathRef path; + CGFloat x = bounds.origin.x, y = bounds.origin.y; + CGColorRef fillColor = IndicatorColor(state, isDark); - CHECK_RADIUS(radius, bounds) - path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); - if (!path) { - return; + CGContextSetFillColorWithColor(context, fillColor); + if (state & TTK_STATE_SELECTED) { + CGContextBeginPath(context); + CGRect dot = {{x + 5, y + 5}, {6, 6}}; + CGContextAddEllipseInRect(context, dot); + CGContextFillPath(context); + } else if (state & TTK_STATE_ALTERNATE) { + CGRect bar = {{x + 4, y + 7}, {8, 2}}; + CGContextFillRect(context, bar); } - CGContextSetFillColorWithColor(context, CGCOLOR(color)); - CGContextBeginPath(context); - CGContextAddPath(context, path); - CGContextFillPath(context); - CFRelease(path); } -/*---------------------------------------------------------------------- - * +++ DrawListHeader -- - * - * This is a standalone drawing procedure which draws column headers for - * a Treeview in the Aqua appearance. The HITheme headers have not - * matched the native ones since OSX 10.8. Note that the header image is - * ignored, but we draw arrows according to the state. - */ - -static void DrawListHeader( - CGRect bounds, +static void +DrawHelpSymbol( CGContextRef context, - Tk_Window tkwin, + CGRect bounds, int state) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *strokeColor, *bgColor; - static CGFloat borderRGBA[4] = { - 200.0 / 255, 200.0 / 255, 200.0 / 255, 1.0 - }; - static CGFloat separatorRGBA[4] = { - 220.0 / 255, 220.0 / 255, 220.0 / 255, 1.0 + NSFont *font = [NSFont controlContentFontOfSize:15]; + NSColor *foreground = state & TTK_STATE_DISABLED ? + [NSColor disabledControlTextColor] : [NSColor controlTextColor]; + NSDictionary *attrs = @{ + NSForegroundColorAttributeName : foreground, + NSFontAttributeName : font }; - static CGFloat activeBgRGBA[4] = { - 238.0 / 255, 238.0 / 255, 238.0 / 255, 1.0 - }; - static CGFloat inactiveBgRGBA[4] = { - 246.0 / 255, 246.0 / 255, 246.0 / 255, 1.0 - }; - - /* - * Apple changes the background of a list header when the window is not - * active. But Ttk does not indicate that in the state of a TreeHeader. - * So we have to query the Apple window manager. - */ - - NSWindow *win = TkMacOSXGetNSWindowForDrawable(Tk_WindowId(tkwin)); - CGFloat *bgRGBA = [win isKeyWindow] ? activeBgRGBA : inactiveBgRGBA; - CGFloat x = bounds.origin.x, y = bounds.origin.y; - CGFloat w = bounds.size.width, h = bounds.size.height; - CGPoint top[2] = {{x, y + 1}, {x + w, y + 1}}; - CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; - CGPoint separator[2] = {{x + w - 1, y + 3}, {x + w - 1, y + h - 3}}; - - bgColor = [NSColor colorWithColorSpace: deviceRGB - components: bgRGBA - count: 4]; + NSAttributedString *attributedString = [[NSAttributedString alloc] + initWithString:@"?" + attributes:attrs]; + CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString( + (CFAttributedStringRef)attributedString); + CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(0, 1)); + CGAffineTransform t = CGAffineTransformMake( + 1.0, 0.0, 0.0, -1.0, 0.0, bounds.size.height); CGContextSaveGState(context); - CGContextSetShouldAntialias(context, false); - CGContextSetFillColorSpace(context, deviceRGB.CGColorSpace); - CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); - CGContextBeginPath(context); - CGContextSetFillColorWithColor(context, CGCOLOR(bgColor)); - CGContextAddRect(context, bounds); - CGContextFillPath(context); - strokeColor = [NSColor colorWithColorSpace: deviceRGB - components: separatorRGBA - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); - CGContextAddLines(context, separator, 2); - CGContextStrokePath(context); - strokeColor = [NSColor colorWithColorSpace: deviceRGB - components: borderRGBA - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); - CGContextAddLines(context, top, 2); - CGContextStrokePath(context); - CGContextAddLines(context, bottom, 2); - CGContextStrokePath(context); + CGContextSetTextMatrix(context, t); + CGContextSetTextPosition(context, + bounds.origin.x + 6.5, + bounds.origin.y + bounds.size.height - 5); + CTLineDraw(line, context); CGContextRestoreGState(context); - - if (state & TTK_TREEVIEW_STATE_SORTARROW) { - CGRect arrowBounds = bounds; - arrowBounds.origin.x = bounds.origin.x + bounds.size.width - 16; - arrowBounds.size.width = 16; - if (state & TTK_STATE_ALTERNATE) { - DrawUpArrow(context, arrowBounds, 3, 8, BLACKRGBA); - } else if (state & TTK_STATE_SELECTED) { - DrawDownArrow(context, arrowBounds, 3, 8, BLACKRGBA); - } - } + CFRelease(line); + CFRelease(typesetter); + [attributedString release]; } + + /*---------------------------------------------------------------------- - * +++ Drawing procedures for widgets in Apple's "Dark Mode" (10.14 and up). - * - * The HIToolbox does not support Dark Mode, and apparently never will, - * so to make widgets look "native" we have to provide analogues of the - * HITheme drawing functions to be used in DarkAqua. We continue to use - * HITheme in Aqua, since it understands earlier versions of the OS. - * - * Drawing the dark widgets requires NSColors that were introduced in OSX - * 10.14, so we make some of these functions be no-ops when building on - * systems older than 10.14. + * +++ Progress bars. */ /*---------------------------------------------------------------------- - * GradientFillRoundedRectangle -- + * DrawProgressBar -- * - * Fill a rounded rectangle with a specified gradient. + * Draws a progress bar, with parameters supplied by a HIThemeTrackDrawInfo + * struct. Draws a rounded rectangular track overlayed by a colored + * rounded rectangular indicator. An indeterminate progress bar is + * animated. */ -static void GradientFillRoundedRectangle( +static void DrawProgressBar( CGContextRef context, CGRect bounds, - CGFloat radius, - CGFloat *colors, - int numColors) + HIThemeTrackDrawInfo info, + int state, + Tk_Window tkwin) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - CGPathRef path; - CHECK_RADIUS(radius, bounds) + CGRect colorBounds; + CGFloat rgba[4]; + CGColorRef trackColor, highlightColor, fillColor; + NSColor *accent; + CGFloat ratio = (CGFloat) info.value / (CGFloat) (info.max - info.min); - CGPoint end = { - bounds.origin.x, - bounds.origin.y + bounds.size.height - }; - CGGradientRef gradient = CGGradientCreateWithColorComponents( - deviceRGB.CGColorSpace, colors, NULL, numColors); - - path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); - CGContextBeginPath(context); - CGContextAddPath(context, path); - CGContextClip(context); - CGContextDrawLinearGradient(context, gradient, bounds.origin, end, 0); - CFRelease(path); - CFRelease(gradient); -} - -/*---------------------------------------------------------------------- - * +++ DrawDarkButton -- - * - * This is a standalone drawing procedure which draws PushButtons and - * PopupButtons in the Dark Mode style. - */ - -static void DrawDarkButton( - CGRect bounds, - ThemeButtonKind kind, - Ttk_State state, - CGContextRef context) -{ - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *faceColor; + GetBackgroundColorRGBA(context, tkwin, 0, NO, rgba); /* - * To match the appearance of Apple's buttons we need to increase the - * height by 1 pixel. + * Compute the bounds for the track and indicator. The track is 6 pixels + * wide in the center of the widget bounds. */ - bounds.size.height += 1; - - CGContextClipToRect(context, bounds); - FillButtonBackground(context, bounds, 5); + if (info.attributes & kThemeTrackHorizontal) { + bounds = CGRectInset(bounds, 1, bounds.size.height / 2 - 3); + colorBounds = bounds; + if (info.kind == kThemeIndeterminateBar) { + CGFloat width = 0.25*bounds.size.width; + CGFloat travel = 0.75*bounds.size.width; + CGFloat center = bounds.origin.x + (width / 2) + ratio*travel; + colorBounds.origin.x = center - width / 2; + colorBounds.size.width = width; + } else { + colorBounds.size.width = ratio*bounds.size.width; + } + if (colorBounds.size.width > 0 && colorBounds.size.width < 6) { + colorBounds.size.width = 6; + } + } else { + bounds = CGRectInset(bounds, bounds.size.width / 2 - 3, 1); + colorBounds = bounds; + if (info.kind == kThemeIndeterminateBar) { + CGFloat height = 0.25*bounds.size.height; + CGFloat travel = 0.75*bounds.size.height; + CGFloat center = bounds.origin.y + (height / 2) + ratio*travel; + colorBounds.origin.y = center - height / 2; + colorBounds.size.height = height; + } else { + colorBounds.size.height = ratio*(bounds.size.height); + } + if (colorBounds.size.height > 0 && colorBounds.size.height < 6) { + colorBounds.size.height = 6; + } + colorBounds.origin.y += bounds.size.height - colorBounds.size.height; + } /* - * Fill the button face with the appropriate color. + * Compute the colors for the track and indicator. */ - bounds = CGRectInset(bounds, 1, 1); - if (kind == kThemePushButton && (state & TTK_STATE_PRESSED)) { - if ([NSApp macOSVersion] < 120000) { - GradientFillRoundedRectangle(context, bounds, 4, - pressedPushButtonGradient, 2); - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkPressedButtonFace - count: 4]; - SolidFillRoundedRectangle(context, bounds, 4, faceColor); + if (TkMacOSXInDarkMode(tkwin)) { + for(int i=0; i < 3; i++) { + rgba[i] += 30.0 / 255.0; } - } else if (kind == kThemePushButton && - (state & TTK_STATE_ALTERNATE) && - !(state & TTK_STATE_BACKGROUND)) { - GradientFillRoundedRectangle(context, bounds, 4, - darkSelectedGradient, 2); + trackColor = CGColorFromRGBA(rgba); + for(int i=0; i < 3; i++) { + rgba[i] -= 5.0 / 255.0; + } + highlightColor = CGColorFromRGBA(rgba); + FillRoundedRectangle(context, bounds, 3, trackColor); } else { - if (state & TTK_STATE_DISABLED) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkDisabledButtonFace - count: 4]; - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkButtonFace - count: 4]; + for(int i=0; i < 3; i++) { + rgba[i] -= 14.0 / 255.0; } - SolidFillRoundedRectangle(context, bounds, 4, faceColor); + trackColor = CGColorFromRGBA(rgba); + for(int i=0; i < 3; i++) { + rgba[i] += 3.0 / 255.0; + } + highlightColor = CGColorFromRGBA(rgba); + bounds.size.height -= 1; + bounds = CGRectInset(bounds, 0, -1); + } + if (state & TTK_STATE_BACKGROUND) { + accent = [NSColor colorWithRed:0.72 green:0.72 blue:0.72 alpha:0.72]; + } else { + accent = controlAccentColor(); } /* - * If this is a popup, draw the arrow button. + * Draw the track, with highlighting around the edge. */ - if ((kind == kThemePopupButton) | (kind == kThemeComboBox)) { - CGRect arrowBounds = bounds; - arrowBounds.size.width = 16; - arrowBounds.origin.x += bounds.size.width - 16; + FillRoundedRectangle(context, bounds, 3, trackColor); + bounds = CGRectInset(bounds, 0, 0.5); + FillRoundedRectangle(context, bounds, 2.5, highlightColor); + bounds = CGRectInset(bounds, 0.5, 0.5); + FillRoundedRectangle(context, bounds, 2, trackColor); + bounds = CGRectInset(bounds, -0.5, -1); - /* - * If the toplevel is front, paint the button blue. - */ + /* + * Draw the indicator. Make it slightly transparent around the + * edge so the highlightng shows through. + */ - if (!(state & TTK_STATE_BACKGROUND) && - !(state & TTK_STATE_DISABLED)) { - GradientFillRoundedRectangle(context, arrowBounds, 4, - darkSelectedGradient, 2); - } - if (kind == kThemePopupButton) { - DrawUpDownArrows(context, arrowBounds, 3, 7, WHITERGBA); - } else { - DrawDownArrow(context, arrowBounds, 4, 8, WHITERGBA); - } + if (info.kind == kThemeIndeterminateBar && + (state & TTK_STATE_SELECTED) == 0) { + return; } - HighlightButtonBorder(context, bounds); + fillColor = CGCOLOR([accent colorWithAlphaComponent:0.9]); + FillRoundedRectangle(context, colorBounds, 3, fillColor); + colorBounds = CGRectInset(colorBounds, 1, 1); + fillColor = CGCOLOR([accent colorWithAlphaComponent:1.0]); + FillRoundedRectangle(context, colorBounds, 2.5, fillColor); } + +/*---------------------------------------------------------------------- + * +++ Sliders. + */ /*---------------------------------------------------------------------- - * +++ DrawDarkIncDecButton -- + * DrawSlider -- + * + * Draws a slider track and round thumb for a Ttk scale widget. The accent + * color is used on the left or top part of the track, so the fraction of + * the track which is colored is equal to (value - from) / (to - from). * - * This is a standalone drawing procedure which draws an IncDecButton - * (as used in a Spinbox) in the Dark Mode style. */ -static void DrawDarkIncDecButton( +static void DrawSlider( + CGContextRef context, CGRect bounds, - ThemeDrawState drawState, - Ttk_State state, - CGContextRef context) + HIThemeTrackDrawInfo info, + int state, + Tk_Window tkwin) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *faceColor; - - bounds = CGRectInset(bounds, 0, -1); - CGContextClipToRect(context, bounds); - FillButtonBackground(context, bounds, 6); + CGColorRef trackColor; + CGRect clipBounds, trackBounds, thumbBounds; + CGPoint thumbPoint; + CGFloat position; + CGColorRef accentColor; + Bool fromIsSmaller = info.reserved; + double from = info.min, to = fabs((double) info.max), value = info.value; /* - * Fill the button face with the appropriate color. + * info.min, info.max and info.value are integers. When this is called + * we will have arranged that min = 0 and max is a large positive integer. */ - bounds = CGRectInset(bounds, 1, 1); - if (state & TTK_STATE_DISABLED) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkDisabledButtonFace - count: 4]; - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkButtonFace - count: 4]; - } - SolidFillRoundedRectangle(context, bounds, 4, faceColor); - - /* - * If pressed, paint the appropriate half blue. - */ + double fraction = (from < to) ? (value - from) / (to - from) : 0.5; + int isDark = TkMacOSXInDarkMode(tkwin); - if (state & TTK_STATE_PRESSED) { - CGRect clip = bounds; - clip.size.height /= 2; - CGContextSaveGState(context); - if (drawState == kThemeStatePressedDown) { - clip.origin.y += clip.size.height; + if (info.attributes & kThemeTrackHorizontal) { + trackBounds = CGRectInset(bounds, 0, bounds.size.height / 2 - 3); + trackBounds.size.height = 3; + position = 8 + fraction * (trackBounds.size.width - 16); + clipBounds = trackBounds; + if (fromIsSmaller) { + clipBounds.size.width = position; + } else { + clipBounds.origin.x += position; + clipBounds.size.width -= position; } - CGContextClipToRect(context, clip); - GradientFillRoundedRectangle(context, bounds, 5, - darkSelectedGradient, 2); - CGContextRestoreGState(context); + thumbPoint = CGPointMake(trackBounds.origin.x + position, + trackBounds.origin.y + 1); + } else { + trackBounds = CGRectInset(bounds, bounds.size.width / 2 - 3, 0); + trackBounds.size.width = 3; + position = 8 + fraction * (trackBounds.size.height - 16); + clipBounds = trackBounds; + if (fromIsSmaller) { + clipBounds.size.height = position; + } else { + clipBounds.origin.y += position; + clipBounds.size.height -= position; + } + thumbPoint = CGPointMake(trackBounds.origin.x + 1, + trackBounds.origin.y + position); + } + trackColor = isDark ? CGColorFromGray(darkTrack): + CGColorFromGray(lightTrack); + thumbBounds = CGRectMake(thumbPoint.x - 8, thumbPoint.y - 8, 17, 17); + CGContextSaveGState(context); + FillRoundedRectangle(context, trackBounds, 1.5, trackColor); + CGContextClipToRect(context, clipBounds); + if (state & (TTK_STATE_BACKGROUND | TTK_STATE_DISABLED)) { + accentColor = isDark ? CGColorFromGray(darkInactiveTrack) : + CGColorFromGray(lightInactiveTrack); + } else { + accentColor = CGCOLOR(controlAccentColor()); } - DrawUpDownArrows(context, bounds, 3, 5, WHITERGBA); - HighlightButtonBorder(context, bounds); + FillRoundedRectangle(context, trackBounds, 1.5, accentColor); + CGContextRestoreGState(context); + DrawGrayButton(context, thumbBounds, &sliderDesign, state, tkwin); } /*---------------------------------------------------------------------- - * +++ DrawDarkBevelButton -- + * +++ Drawing procedures for native widgets. * - * This is a standalone drawing procedure which draws RoundedBevelButtons - * in the Dark Mode style. + * The HIToolbox does not support Dark Mode, and apparently never will. + * It also draws some widgets in discontinued older styles even when used + * on new OS releases. So to make widgets look "native" we have to provide + * analogues of the HIToolbox drawing functions to be used on newer systems. + * We continue to use NIToolbox for older versions of the OS. + * + * Drawing the dark widgets requires NSColors that were introduced in OSX + * 10.14, so we make some of these functions be no-ops when building on + * systems older than 10.14. */ -static void DrawDarkBevelButton( +/*---------------------------------------------------------------------- + * DrawButton -- + * + * This is a standalone drawing procedure which draws most types of macOS + * buttons for newer OS releases. The button style is specified in the + * "kind" field of a HIThemeButtonDrawInfo struct, although some of the + * identifiers are not recognized by HIToolbox. + */ + +static void DrawButton( CGRect bounds, + HIThemeButtonDrawInfo info, Ttk_State state, - CGContextRef context) + CGContextRef context, + Tk_Window tkwin) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *faceColor; + ThemeButtonKind kind = info.kind; + ThemeDrawState drawState = info.state; + CGRect arrowBounds = bounds = CGRectInset(bounds, 1, 1); + int hasIndicator, isDark = TkMacOSXInDarkMode(tkwin); - CGContextClipToRect(context, bounds); - FillButtonBackground(context, bounds, 5); + switch (kind) { + case TkRoundedRectButton: + DrawGrayButton(context, bounds, &roundedrectDesign, state, tkwin); + break; + case TkInlineButton: + DrawGrayButton(context, bounds, &inlineDesign, state, tkwin); + break; + case TkRecessedButton: + DrawGrayButton(context, bounds, &recessedDesign, state, tkwin); + break; + case kThemeRoundedBevelButton: + DrawGrayButton(context, bounds, &bevelDesign, state, tkwin); + break; + case kThemePushButton: - /* - * Fill the button face with the appropriate color. - */ + /* + * The TTK_STATE_ALTERNATE bit means -default active. Apple only + * indicates the default state (which means that the key equivalent is + * "\n") for Push Buttons. + */ - bounds = CGRectInset(bounds, 1, 1); - if (state & TTK_STATE_PRESSED) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkPressedBevelFace - count: 4]; - } else if ((state & TTK_STATE_DISABLED) || - (state & TTK_STATE_ALTERNATE)) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkDisabledButtonFace - count: 4]; - } else if (state & TTK_STATE_SELECTED) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkSelectedBevelFace - count: 4]; - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkButtonFace - count: 4]; + if ((state & TTK_STATE_PRESSED || state & TTK_STATE_ALTERNATE) && + !(state & TTK_STATE_BACKGROUND)) { + DrawAccentedButton(context, bounds, &pushbuttonDesign, state, isDark); + } else { + DrawGrayButton(context, bounds, &pushbuttonDesign, state, tkwin); + } + break; + case kThemeRoundButtonHelp: + DrawGrayButton(context, bounds, &helpDesign, state, tkwin); + DrawHelpSymbol(context, bounds, state); + break; + case kThemePopupButton: + drawState = 0; + DrawGrayButton(context, bounds, &popupDesign, state, tkwin); + arrowBounds.size.width = 17; + arrowBounds.origin.x += bounds.size.width - 17; + if (!(state & TTK_STATE_BACKGROUND) && + !(state & TTK_STATE_DISABLED)) { + CGRect popupBounds = arrowBounds; + + /* + * Allow room for nonexistent focus ring. + */ + + popupBounds.size.width += 4; + popupBounds.origin.y -= 4; + popupBounds.size.height += 8; + DrawAccentedSegment(context, popupBounds, &popupDesign, state, tkwin); + drawState = BOTH_ARROWS; + } + arrowBounds.origin.x += 2; + DrawUpDownArrows(context, arrowBounds, 3, 7, 2, state, drawState); + break; + case kThemeComboBox: + if (state & TTK_STATE_DISABLED) { + // Need to add the disabled case to entryDesign. + DrawEntry(context, bounds, &entryDesign, state, tkwin); + } else { + DrawEntry(context, bounds, &entryDesign, state, tkwin); + } + arrowBounds.size.width = 17; + if (state & TTK_STATE_BACKGROUND) { + arrowBounds.origin.x += bounds.size.width - 20; + arrowBounds.size.width += 4; + arrowBounds.origin.y -= 1; + } else { + arrowBounds.origin.y -= 1; + arrowBounds.origin.x += bounds.size.width - 20; + arrowBounds.size.width += 4; + arrowBounds.size.height += 2; + } + DrawAccentedSegment(context, arrowBounds, &comboDesign, state, tkwin); + if (!(state & TTK_STATE_BACKGROUND)) { + state |= TTK_STATE_IS_ACCENTED; + } + DrawDownArrow(context, arrowBounds, 6, 6, state); + break; + case kThemeCheckBox: + bounds = CGRectOffset(CGRectMake(0, bounds.size.height / 2 - 8, 16, 16), + bounds.origin.x, bounds.origin.y); + bounds = CGRectInset(bounds, 1, 1); + hasIndicator = state & TTK_STATE_SELECTED || state & TTK_STATE_ALTERNATE; + if (hasIndicator && + !(state & TTK_STATE_BACKGROUND) && + !(state & TTK_STATE_DISABLED)) { + DrawAccentedButton(context, bounds, &checkDesign, 0, isDark); + } else { + DrawGrayButton(context, bounds, &checkDesign, state, tkwin); + } + if (hasIndicator) { + DrawCheckIndicator(context, bounds, state, isDark); + } + break; + case kThemeRadioButton: + bounds = CGRectOffset(CGRectMake(0, bounds.size.height / 2 - 9, 18, 18), + bounds.origin.x, bounds.origin.y); + bounds = CGRectInset(bounds, 1, 1); + hasIndicator = state & TTK_STATE_SELECTED || state & TTK_STATE_ALTERNATE; + if (hasIndicator && + !(state & TTK_STATE_BACKGROUND) && + !(state & TTK_STATE_DISABLED)) { + DrawAccentedButton(context, bounds, &radioDesign, 0, isDark); + } else { + DrawGrayButton(context, bounds, &radioDesign, state, tkwin); + } + if (hasIndicator) { + DrawRadioIndicator(context, bounds, state, isDark); + } + break; + case kThemeArrowButton: + DrawGrayButton(context, bounds, &pushbuttonDesign, state, tkwin); + arrowBounds.origin.x = bounds.origin.x + bounds.size.width - 17; + arrowBounds.size.width = 16; + arrowBounds.origin.y -= 1; + if (state & TTK_STATE_SELECTED) { + DrawUpArrow(context, arrowBounds, 5, 6, state); + } else { + DrawDownArrow(context, arrowBounds, 5, 6, state); + } + break; + case kThemeIncDecButton: + DrawGrayButton(context, bounds, &incdecDesign, state, tkwin); + if (state & TTK_STATE_PRESSED) { + CGRect clip; + if (drawState == kThemeStatePressedDown) { + clip = bounds; + clip.size.height /= 2; + clip.origin.y += clip.size.height; + bounds.size.height += 1; + clip.size.height += 1; + } else { + clip = bounds; + clip.size.height /= 2; + } + CGContextSaveGState(context); + CGContextClipToRect(context, clip); + DrawAccentedButton(context, bounds, &incdecDesign, 0, isDark); + CGContextRestoreGState(context); + } + { + CGFloat inset = (bounds.size.width - 5) / 2; + DrawUpDownArrows(context, bounds, inset, 5, 3, state, drawState); + } + break; + default: + break; } - SolidFillRoundedRectangle(context, bounds, 4, faceColor); - HighlightButtonBorder(context, bounds); } - + /*---------------------------------------------------------------------- - * +++ DrawDarkCheckBox -- + * DrawGroupBox -- * - * This is a standalone drawing procedure which draws Checkboxes in the - * Dark Mode style. + * This is a standalone drawing procedure which draws the contrasting rounded + * rectangular box for LabelFrames and Notebook panes used in more recent + * versions of macOS. Normally the contrast is set to one, since the nesting + * level of the Group Box is higher by 1 compared to its container. But we + * allow higher contrast for special cases, notably notebook tabs in macOS 11. + * The save parameter is passed to GetBackgroundColor and should probably be + * NO in such special cases. */ -static void DrawDarkCheckBox( +static void DrawGroupBox( CGRect bounds, - Ttk_State state, - CGContextRef context) + CGContextRef context, + Tk_Window tkwin, + int contrast, + Bool save) { - CGRect checkbounds = {{0, bounds.size.height / 2 - 8}, {16, 16}}; - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *stroke; - CGFloat x, y; + CHECK_RADIUS(5, bounds) - bounds = CGRectOffset(checkbounds, bounds.origin.x, bounds.origin.y); - x = bounds.origin.x; - y = bounds.origin.y; + CGPathRef path; + CGColorRef backgroundColor, borderColor; + backgroundColor = GetBackgroundCGColor(context, tkwin, contrast, save); + borderColor = CGColorFromGray(boxBorder); + CGContextSetFillColorWithColor(context, backgroundColor); + path = CGPathCreateWithRoundedRect(bounds, 5, 5, NULL); CGContextClipToRect(context, bounds); - FillButtonBackground(context, bounds, 4); - bounds = CGRectInset(bounds, 1, 1); - if (!(state & TTK_STATE_BACKGROUND) && - !(state & TTK_STATE_DISABLED) && - ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE))) { - GradientFillRoundedRectangle(context, bounds, 3, - darkSelectedGradient, 2); - } else { - GradientFillRoundedRectangle(context, bounds, 3, - darkInactiveGradient, 2); - } - HighlightButtonBorder(context, bounds); - if ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE)) { - CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); - if (state & TTK_STATE_DISABLED) { - stroke = [NSColor disabledControlTextColor]; - } else { - stroke = [NSColor controlTextColor]; - } - CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); - } - if (state & TTK_STATE_SELECTED) { - CGContextSetLineWidth(context, 1.5); - CGContextBeginPath(context); - CGPoint check[3] = {{x + 4, y + 8}, {x + 7, y + 11}, {x + 11, y + 4}}; - CGContextAddLines(context, check, 3); - CGContextStrokePath(context); - } else if (state & TTK_STATE_ALTERNATE) { - CGContextSetLineWidth(context, 2.0); - CGContextBeginPath(context); - CGPoint bar[2] = {{x + 4, y + 8}, {x + 12, y + 8}}; - CGContextAddLines(context, bar, 2); - CGContextStrokePath(context); - } + CGContextBeginPath(context); + CGContextAddPath(context, path); + CGContextFillPath(context); + CGContextSetFillColorWithColor(context, borderColor); + CGContextBeginPath(context); + CGContextAddPath(context, path); + CGContextReplacePathWithStrokedPath(context); + CGContextFillPath(context); + CFRelease(path); } - + /*---------------------------------------------------------------------- - * +++ DrawDarkRadioButton -- + * DrawListHeader -- * - * This is a standalone drawing procedure which draws RadioButtons - * in the Dark Mode style. + * This is a standalone drawing procedure which draws column headers for a + * Treeview in the Aqua appearance. (The HIToolbox headers have not matched the + * native ones since OSX 10.8) Note that the header image is ignored, but we + * draw arrows according to the state. */ -static void DrawDarkRadioButton( +static void DrawListHeader( CGRect bounds, - Ttk_State state, - CGContextRef context) + CGContextRef context, + Tk_Window tkwin, + int state) { - CGRect checkbounds = {{0, bounds.size.height / 2 - 9}, {18, 18}}; - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *fill; - CGFloat x, y; + int isDark = TkMacOSXInDarkMode(tkwin); + CGFloat x = bounds.origin.x, y = bounds.origin.y; + CGFloat w = bounds.size.width, h = bounds.size.height; + CGPoint top[2] = {{x, y + 1}, {x + w, y + 1}}; + CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; + CGPoint separator[2] = {{x + w - 1, y + 3}, {x + w - 1, y + h - 3}}; + CGColorRef strokeColor, backgroundColor; - bounds = CGRectOffset(checkbounds, bounds.origin.x, bounds.origin.y); - x = bounds.origin.x; - y = bounds.origin.y; + /* + * Apple changes the background color of a list header when the window is + * not active. But Ttk does not indicate that in the state of a + * TreeHeader. So we have to query the Apple window manager. + */ - CGContextClipToRect(context, bounds); - FillButtonBackground(context, bounds, 9); - bounds = CGRectInset(bounds, 1, 1); - if (!(state & TTK_STATE_BACKGROUND) && - !(state & TTK_STATE_DISABLED) && - ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE))) { - GradientFillRoundedRectangle(context, bounds, 8, - darkSelectedGradient, 2); - } else { - GradientFillRoundedRectangle(context, bounds, 8, - darkInactiveGradient, 2); - } - HighlightButtonBorder(context, bounds); - if ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE)) { - CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); - if (state & TTK_STATE_DISABLED) { - fill = [NSColor disabledControlTextColor]; - } else { - fill = [NSColor controlTextColor]; - } - CGContextSetFillColorWithColor(context, CGCOLOR(fill)); + NSWindow *win = TkMacOSXGetNSWindowForDrawable(Tk_WindowId(tkwin)); + if (!isDark) { + GrayColor bgGray = [win isKeyWindow] ? + listheaderActiveBG : listheaderInactiveBG; + backgroundColor = CGColorFromGray(bgGray); } - if (state & TTK_STATE_SELECTED) { + + CGContextSaveGState(context); + CGContextSetShouldAntialias(context, false); + if (!isDark) { CGContextBeginPath(context); - CGRect dot = {{x + 6, y + 6}, {6, 6}}; - CGContextAddEllipseInRect(context, dot); + CGContextSetFillColorWithColor(context, backgroundColor); + CGContextAddRect(context, bounds); CGContextFillPath(context); - } else if (state & TTK_STATE_ALTERNATE) { - CGRect bar = {{x + 5, y + 8}, {8, 2}}; - CGContextFillRect(context, bar); } -} + strokeColor = isDark ? + CGColorFromGray(darkListheaderBorder) : + CGColorFromGray(listheaderSeparator); + CGContextSetStrokeColorWithColor(context, strokeColor); + CGContextAddLines(context, separator, 2); + CGContextStrokePath(context); + strokeColor = isDark ? + CGColorFromGray(darkListheaderBorder) : + CGColorFromGray(lightListheaderBorder); + CGContextSetStrokeColorWithColor(context, strokeColor); + CGContextAddLines(context, top, 2); + CGContextStrokePath(context); + CGContextAddLines(context, bottom, 2); + CGContextStrokePath(context); + CGContextRestoreGState(context); + if (state & TTK_TREEVIEW_STATE_SORTARROW) { + CGRect arrowBounds = bounds; + arrowBounds.origin.x = bounds.origin.x + bounds.size.width - 16; + arrowBounds.size.width = 16; + if (state & TTK_STATE_ALTERNATE) { + DrawUpArrow(context, arrowBounds, 3, 8, state); + } else if (state & TTK_STATE_SELECTED) { + DrawDownArrow(context, arrowBounds, 3, 8, state); + } + } +} + /*---------------------------------------------------------------------- - * +++ DrawDarkTab -- + * DrawTab -- * - * This is a standalone drawing procedure which draws Tabbed Pane - * Tabs in the Dark Mode style. + * This is a standalone drawing procedure which draws Tabbed Pane Tabs for the + * notebook widget. */ -static void DrawDarkTab( +static void +DrawTab( CGRect bounds, Ttk_State state, - CGContextRef context) + CGContextRef context, + Tk_Window tkwin) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *faceColor, *stroke; CGRect originalBounds = bounds; + CGColorRef strokeColor; int OSVersion = [NSApp macOSVersion]; - CGContextSetLineWidth(context, 1.0); - CGContextClipToRect(context, bounds); - /* * Extend the bounds to one or both sides so the rounded part will be - * clipped off. + * clipped off if the right of the left tab, the left of the right tab, + * and both sides of the middle tabs. */ + CGContextClipToRect(context, bounds); if (OSVersion < 110000 || !(state & TTK_STATE_SELECTED)) { if (!(state & TTK_STATE_FIRST_TAB)) { bounds.origin.x -= 10; @@ -1031,37 +1490,14 @@ static void DrawDarkTab( bounds.size.width += 10; } } - /* * Fill the tab face with the appropriate color or gradient. Use a solid - * color if the tab is not selected, otherwise use a blue or gray - * gradient. + * color if the tab is not selected, otherwise use the accent color with + * highlights */ - bounds = CGRectInset(bounds, 1, 1); if (!(state & TTK_STATE_SELECTED)) { - if (state & TTK_STATE_DISABLED) { - if (OSVersion < 110000) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkDisabledButtonFace - count: 4]; - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkTab - count: 4]; - } - } else { - if (OSVersion < 110000) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkButtonFace - count: 4]; - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkTab - count: 4]; - } - } - SolidFillRoundedRectangle(context, bounds, 4, faceColor); + DrawGrayButton(context, bounds, &tabDesign, state, tkwin); /* * Draw a separator line on the left side of the tab if it @@ -1070,11 +1506,8 @@ static void DrawDarkTab( if (!(state & TTK_STATE_FIRST_TAB)) { CGContextSaveGState(context); - CGContextSetShouldAntialias(context, false); - stroke = [NSColor colorWithColorSpace: deviceRGB - components: darkTabSeparator - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); + strokeColor = CGColorFromGray(darkTabSeparator); + CGContextSetStrokeColorWithColor(context, strokeColor); CGContextBeginPath(context); CGContextMoveToPoint(context, originalBounds.origin.x, originalBounds.origin.y + 1); @@ -1085,301 +1518,119 @@ static void DrawDarkTab( } } else { - /* - * This is the selected tab. If it is first, cover up the separator - * line drawn by the second one. (The selected tab is always drawn - * last.) - */ + /* + * This is the selected tab; paint it with the current accent color. + * If it is first, cover up the separator line drawn by the second one. + * (The selected tab is always drawn last.) + */ if ((state & TTK_STATE_FIRST_TAB) && !(state & TTK_STATE_LAST_TAB)) { bounds.size.width += 1; } if (!(state & TTK_STATE_BACKGROUND)) { - if (OSVersion < 110000) { - GradientFillRoundedRectangle(context, bounds, 4, - darkSelectedGradient, 2); - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkSelectedTab - count: 4]; - SolidFillRoundedRectangle(context, bounds, 4, faceColor); - } + DrawAccentedButton(context, bounds, &tabDesign, 0, 0); } else { - if (OSVersion < 110000) { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkInactiveSelectedTab - count: 4]; - } else { - faceColor = [NSColor colorWithColorSpace: deviceRGB - components: darkSelectedTab - count: 4]; - } - SolidFillRoundedRectangle(context, bounds, 4, faceColor); + DrawGrayButton(context, bounds, &tabDesign, state, tkwin); } - HighlightButtonBorder(context, bounds); } } -/*---------------------------------------------------------------------- - * +++ DrawDarkSeparator -- - * - * This is a standalone drawing procedure which draws a separator widget - * in Dark Mode. - */ - -static void DrawDarkSeparator( - CGRect bounds, - CGContextRef context, - TCL_UNUSED(Tk_Window)) -{ - static CGFloat fill[4] = {1.0, 1.0, 1.0, 0.3}; - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *fillColor = [NSColor colorWithColorSpace: deviceRGB - components: fill - count:4]; - - CGContextSetFillColorWithColor(context, CGCOLOR(fillColor)); - CGContextFillRect(context, bounds); -} - -/*---------------------------------------------------------------------- - * +++ DrawDarkFocusRing -- - * - * This is a standalone drawing procedure which draws a focus ring around - * an Entry widget in Dark Mode. - */ - -static void DrawDarkFocusRing( - CGRect bounds, - CGContextRef context) -{ - CGRect insetBounds = CGRectInset(bounds, -3, -3); - CHECK_RADIUS(4, insetBounds) - - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *strokeColor; - NSColor *fillColor = [NSColor colorWithColorSpace:deviceRGB - components:darkFocusRing - count:4]; - CGFloat x = bounds.origin.x, y = bounds.origin.y; - CGFloat w = bounds.size.width, h = bounds.size.height; - CGPoint topPart[4] = { - {x, y + h}, {x, y + 1}, {x + w - 1, y + 1}, {x + w - 1, y + h} - }; - CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; - - CGContextSaveGState(context); - CGContextSetShouldAntialias(context, false); - CGContextBeginPath(context); - strokeColor = [NSColor colorWithColorSpace: deviceRGB - components: darkFocusRingTop - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); - CGContextAddLines(context, topPart, 4); - CGContextStrokePath(context); - strokeColor = [NSColor colorWithColorSpace: deviceRGB - components: darkFocusRingBottom - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); - CGContextAddLines(context, bottom, 2); - CGContextStrokePath(context); - CGContextSetShouldAntialias(context, true); - CGContextSetFillColorWithColor(context, CGCOLOR(fillColor)); - CGPathRef path = CGPathCreateWithRoundedRect(insetBounds, 4, 4, NULL); - CGContextBeginPath(context); - CGContextAddPath(context, path); - CGPathRelease(path); - CGContextAddRect(context, bounds); - CGContextEOFillPath(context); - CGContextRestoreGState(context); -} -/*---------------------------------------------------------------------- - * +++ DrawDarkFrame -- - * - * This is a standalone drawing procedure which draws various - * types of borders in Dark Mode. - */ - -static void DrawDarkFrame( +static void +DrawTab11( CGRect bounds, + Ttk_State state, CGContextRef context, - HIThemeFrameKind kind) + Tk_Window tkwin) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *stroke; - CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); - CGFloat x = bounds.origin.x, y = bounds.origin.y; - CGFloat w = bounds.size.width, h = bounds.size.height; - CGPoint topPart[4] = { - {x, y + h - 1}, {x, y + 1}, {x + w, y + 1}, {x + w, y + h - 1} - }; - CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; - CGPoint accent[2] = {{x, y + 1}, {x + w, y + 1}}; + if (state & TTK_STATE_SELECTED) { + DrawGrayButton(context, bounds, &pushbuttonDesign, state, tkwin); + } else { + CGRect clipRect = bounds; + /* + * Draw a segment of a Group Box as a background for non-selected tabs. + * Clip the Group Box so that the segments fit together to form a long + * rounded rectangle behind the entire tab bar. + */ - switch (kind) { - case kHIThemeFrameTextFieldSquare: + if (!(state & TTK_STATE_FIRST_TAB)) { + clipRect.origin.x -= 5; + bounds.origin.x -= 5; + bounds.size.width += 5; + } + if (!(state & TTK_STATE_LAST_TAB)) { + clipRect.size.width += 5; + bounds.size.width += 5; + } CGContextSaveGState(context); - CGContextSetShouldAntialias(context, false); - CGContextBeginPath(context); - stroke = [NSColor colorWithColorSpace: deviceRGB - components: darkFrameTop - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); - CGContextAddLines(context, topPart, 4); - CGContextStrokePath(context); - stroke = [NSColor colorWithColorSpace: deviceRGB - components: darkFrameBottom - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); - CGContextAddLines(context, bottom, 2); - CGContextStrokePath(context); - stroke = [NSColor colorWithColorSpace: deviceRGB - components: darkFrameAccent - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); - CGContextAddLines(context, accent, 2); - CGContextStrokePath(context); + CGContextClipToRect(context, clipRect); + DrawGroupBox(bounds, context, tkwin, 3, NO); CGContextRestoreGState(context); - break; - default: - break; } } /*---------------------------------------------------------------------- - * +++ DrawListHeader -- + * DrawDarkSeparator -- * - * This is a standalone drawing procedure which draws column - * headers for a Treeview in the Dark Mode. + * This is a standalone drawing procedure which draws a separator widget + * in Dark Mode. HIToolbox is used in light mode. */ -static void DrawDarkListHeader( +static void DrawDarkSeparator( CGRect bounds, CGContextRef context, - TCL_UNUSED(Tk_Window), - int state) + TCL_UNUSED(Tk_Window)) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *stroke; - - CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); - CGFloat x = bounds.origin.x, y = bounds.origin.y; - CGFloat w = bounds.size.width, h = bounds.size.height; - - CGPoint top[2] = {{x, y + 1}, {x + w, y + 1}}; - CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; - CGPoint separator[2] = {{x + w - 1, y + 3}, {x + w - 1, y + h - 3}}; - CGContextSaveGState(context); - CGContextSetShouldAntialias(context, false); - stroke = [NSColor colorWithColorSpace: deviceRGB - components: darkFrameBottom - count: 4]; - CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); - CGContextBeginPath(context); - CGContextAddLines(context, top, 2); - CGContextStrokePath(context); - CGContextAddLines(context, bottom, 2); - CGContextStrokePath(context); - CGContextAddLines(context, separator, 2); - CGContextStrokePath(context); - CGContextRestoreGState(context); - - if (state & TTK_TREEVIEW_STATE_SORTARROW) { - CGRect arrowBounds = bounds; + CGColorRef sepColor = CGColorFromGray(darkSeparator); - arrowBounds.origin.x = bounds.origin.x + bounds.size.width - 16; - arrowBounds.size.width = 16; - if (state & TTK_STATE_ALTERNATE) { - DrawUpArrow(context, arrowBounds, 3, 8, WHITERGBA); - } else if (state & TTK_STATE_SELECTED) { - DrawDownArrow(context, arrowBounds, 3, 8, WHITERGBA); - } - } + CGContextSetFillColorWithColor(context, sepColor); + CGContextFillRect(context, bounds); } /*---------------------------------------------------------------------- - * +++ Button element: Used for elements drawn with DrawThemeButton. - */ - -/* - * When Ttk draws the various types of buttons, a pointer to one of these - * is passed as the clientData. - */ - -typedef struct { - ThemeButtonKind kind; - ThemeMetric heightMetric; -} ThemeButtonParams; -static ThemeButtonParams - PushButtonParams = {kThemePushButton, kThemeMetricPushButtonHeight}, - CheckBoxParams = {kThemeCheckBox, kThemeMetricCheckBoxHeight}, - RadioButtonParams = {kThemeRadioButton, kThemeMetricRadioButtonHeight}, - BevelButtonParams = {kThemeRoundedBevelButton, NoThemeMetric}, - PopupButtonParams = {kThemePopupButton, kThemeMetricPopupButtonHeight}, - DisclosureParams = { - kThemeDisclosureButton, kThemeMetricDisclosureTriangleHeight -}, - ListHeaderParams = -{kThemeListHeaderButton, kThemeMetricListHeaderHeight}; -static Ttk_StateTable ButtonValueTable[] = { - {kThemeButtonOff, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, - {kThemeButtonMixed, TTK_STATE_ALTERNATE, 0}, - {kThemeButtonOn, TTK_STATE_SELECTED, 0}, - {kThemeButtonOff, 0, 0} - - /* - * Others: kThemeDisclosureRight, kThemeDisclosureDown, - * kThemeDisclosureLeft - */ - -}; -static Ttk_StateTable ButtonAdornmentTable[] = { - {kThemeAdornmentNone, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, - {kThemeAdornmentDefault | kThemeAdornmentFocus, - TTK_STATE_ALTERNATE | TTK_STATE_FOCUS, 0}, - {kThemeAdornmentFocus, TTK_STATE_FOCUS, 0}, - {kThemeAdornmentDefault, TTK_STATE_ALTERNATE, 0}, - {kThemeAdornmentNone, 0, 0} -}; - -/*---------------------------------------------------------------------- - * +++ computeButtonDrawInfo -- + * +++ DrawGradientButton -- * - * Fill in an appearance manager HIThemeButtonDrawInfo record. + * This is a standalone drawing procedure which draws a + * a Gradient Button. */ -static inline HIThemeButtonDrawInfo computeButtonDrawInfo( - ThemeButtonParams *params, - Ttk_State state, - TCL_UNUSED(Tk_Window)) +static void DrawGradientBorder( + CGRect bounds, + CGContextRef context, + Tk_Window tkwin, + Ttk_State state) { + CGColorRef faceColor, borderColor; + GrayColor faceGray, borderGray; + CGRect inside = CGRectInset(bounds, 1, 1); - /* - * See ButtonElementDraw for the explanation of why we always draw - * PushButtons in the active state. - */ - - SInt32 HIThemeState; - - HIThemeState = Ttk_StateTableLookup(ThemeStateTable, state); - switch (params->kind) { - case kThemePushButton: - HIThemeState &= ~kThemeStateInactive; - HIThemeState |= kThemeStateActive; - break; - default: - break; + if (TkMacOSXInDarkMode(tkwin)) { + if (state & TTK_STATE_DISABLED) { + faceGray = darkGradientDisabled; + borderGray = darkGradientBorderDisabled; + } else { + faceGray = state & TTK_STATE_PRESSED ? + darkGradientPressed : darkGradientNormal; + borderGray = darkGradientBorder; + } + } else { + if (state & TTK_STATE_DISABLED) { + faceGray = lightGradientDisabled; + borderGray = lightGradientBorderDisabled; + } else { + faceGray = state & TTK_STATE_PRESSED ? + lightGradientPressed : lightGradientNormal; + borderGray = lightGradientBorder; + } } - - const HIThemeButtonDrawInfo info = { - .version = 0, - .state = HIThemeState, - .kind = params ? params->kind : 0, - .value = Ttk_StateTableLookup(ButtonValueTable, state), - .adornment = Ttk_StateTableLookup(ButtonAdornmentTable, state), - }; - return info; + faceColor = CGColorFromGray(faceGray); + borderColor = CGColorFromGray(borderGray); + CGContextSetFillColorWithColor(context, faceColor); + CGContextFillRect(context, inside); + CGContextSetFillColorWithColor(context, borderColor); + CGContextAddRect(context, bounds); + CGContextAddRect(context, inside); + CGContextEOFillPath(context); } /*---------------------------------------------------------------------- @@ -1388,16 +1639,13 @@ static inline HIThemeButtonDrawInfo computeButtonDrawInfo( static void ButtonElementMinSize( void *clientData, - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), int *minWidth, - int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + int *minHeight) { ThemeButtonParams *params = (ThemeButtonParams *)clientData; if (params->heightMetric != NoThemeMetric) { - ChkErr(GetThemeMetric, params->heightMetric, (SInt *) minHeight); + ChkErr(GetThemeMetric, params->heightMetric, minHeight); /* * The theme height does not include the 1-pixel border around @@ -1408,33 +1656,54 @@ static void ButtonElementMinSize( *minHeight += 2; /* - * The minwidth must be 0 to force the generic ttk code to compute the + * For buttons with labels the minwidth must be 0 to force the * correct text layout. For example, a non-zero value will cause the * text to be left justified, no matter what -anchor setting is used in * the style. */ - *minWidth = 0; + if (params->widthMetric != NoThemeMetric) { + ChkErr(GetThemeMetric, params->widthMetric, minWidth); + *minWidth += 2; + *minHeight += 2; + } else { + *minWidth = 0; + } } } static void ButtonElementSize( void *clientData, - void *elementRecord, + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, int *minWidth, int *minHeight, Ttk_Padding *paddingPtr) { ThemeButtonParams *params = (ThemeButtonParams *)clientData; - const HIThemeButtonDrawInfo info = - computeButtonDrawInfo(params, 0, tkwin); + HIThemeButtonDrawInfo info = + ComputeButtonDrawInfo(params, 0, tkwin); static const CGRect scratchBounds = {{0, 0}, {100, 100}}; CGRect contentBounds, backgroundBounds; int verticalPad; - ButtonElementMinSize(clientData, elementRecord, tkwin, - minWidth, minHeight, paddingPtr); + ButtonElementMinSize(clientData, minWidth, minHeight); + switch (info.kind) { + case TkGradientButton: + *paddingPtr = Ttk_MakePadding(1, 1, 1, 1); + /* Fall through. */ + case kThemeArrowButton: + case kThemeRoundButtonHelp: + return; + /* Buttons which are sized like PushButtons but unknown to HITheme. */ + case TkRoundedRectButton: + case TkRecessedButton: + case TkInlineButton: + info.kind = kThemePushButton; + break; + default: + break; + } /* * Given a hypothetical bounding rectangle for a button, HIToolbox will @@ -1443,7 +1712,7 @@ static void ButtonElementSize( * enough to contain the image of the button in any state, which might * include highlight borders, shadows, etc. The content rectangle is not * centered vertically within the background rectangle, presumably because - * shadows only appear on the bottom. Nonetheless, when HITools is asked + * shadows only appear on the bottom. Nonetheless, when HIToolbox is asked * to draw a button with a certain bounding rectangle it draws the button * centered within the rectangle. * @@ -1462,11 +1731,15 @@ static void ButtonElementSize( CGRectGetMaxX(backgroundBounds) - CGRectGetMaxX(contentBounds); verticalPad = backgroundBounds.size.height - contentBounds.size.height; paddingPtr->top = paddingPtr->bottom = verticalPad / 2; + if (info.kind == kThemePopupButton) { + paddingPtr->top += 1; + paddingPtr->bottom -= 1; + } } static void ButtonElementDraw( void *clientData, - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -1474,71 +1747,97 @@ static void ButtonElementDraw( { ThemeButtonParams *params = (ThemeButtonParams *)clientData; CGRect bounds = BoxToRect(d, b); - HIThemeButtonDrawInfo info = computeButtonDrawInfo(params, state, tkwin); + HIThemeButtonDrawInfo info = ComputeButtonDrawInfo(params, state, tkwin); + int isDark = TkMacOSXInDarkMode(tkwin); - bounds = NormalizeButtonBounds(params->heightMetric, bounds); + switch (info.kind) { - BEGIN_DRAWING(d) - if (TkMacOSXInDarkMode(tkwin)) { - switch (info.kind) { - case kThemePushButton: - case kThemePopupButton: - DrawDarkButton(bounds, info.kind, state, dc.context); - break; - case kThemeCheckBox: - DrawDarkCheckBox(bounds, state, dc.context); - break; - case kThemeRadioButton: - DrawDarkRadioButton(bounds, state, dc.context); - break; - case kThemeRoundedBevelButton: - DrawDarkBevelButton(bounds, state, dc.context); - break; - default: - ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, - HIOrientation, NULL); - } - } else if (info.kind == kThemePushButton && - (state & TTK_STATE_PRESSED)) { - bounds.size.height += 2; - if ([NSApp macOSVersion] > 100800) { - GradientFillRoundedRectangle(dc.context, bounds, 4, - pressedPushButtonGradient, 2); - } - } else { + /* + * A Gradient Button should have an image and no text. The size is set to + * that of the image. All we need to do is draw a 1-pixel border. + */ - /* - * Apple's PushButton and PopupButton do not change their fill color - * when the window is inactive. However, except in 10.7 (Lion), the - * color of the arrow button on a PopupButton does change. For some - * reason HITheme fills inactive buttons with a transparent color that - * allows the window background to show through, leading to - * inconsistent behavior. We work around this by filling behind an - * inactive PopupButton with a text background color before asking - * HIToolbox to draw it. For PushButtons, we simply draw them in the - * active state. - */ + case TkGradientButton: + BEGIN_DRAWING(d) + DrawGradientBorder(bounds, dc.context, tkwin, state); + END_DRAWING + return; + /* + * Buttons with no height restrictions are ready to draw. + */ - if (info.kind == kThemePopupButton && - (state & TTK_STATE_BACKGROUND)) { - CGRect innerBounds = CGRectInset(bounds, 1, 1); - NSColor *whiteRGBA = [NSColor whiteColor]; - SolidFillRoundedRectangle(dc.context, innerBounds, 4, whiteRGBA); - } + case kThemeArrowButton: + case kThemeCheckBox: + case kThemeRadioButton: + break; - /* - * A BevelButton with mixed value is drawn borderless, which does make - * much sense for us. - */ + /* + * Other buttons have a maximum height. We have to deal with that. + */ - if (info.kind == kThemeRoundedBevelButton && - info.value == kThemeButtonMixed) { - info.value = kThemeButtonOff; - info.state = kThemeStateInactive; - } - ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, - NULL); + default: + bounds = NormalizeButtonBounds(params, bounds, isDark); + break; + } + + /* We do our own drawing on new systems.*/ + + if ([NSApp macOSVersion] > 100800) { + BEGIN_DRAWING(d) + DrawButton(bounds, info, state, dc.context, tkwin); + END_DRAWING + return; + } + + /* + * If execution reaches here it means we should use HIToolbox to draw the + * button. Buttons that HIToolbox doesn't know are rendered as + * PushButtons. + */ + + switch (info.kind) { + case TkRoundedRectButton: + case TkRecessedButton: + info.kind = kThemePushButton; + break; + default: + break; + } + + /* + * Apple's PushButton and PopupButton do not change their fill color + * when the window is inactive. However, except in 10.7 (Lion), the + * color of the arrow button on a PopupButton does change. For some + * reason HITheme fills inactive buttons with a transparent color that + * allows the window background to show through, leading to + * inconsistent behavior. We work around this by filling behind an + * inactive PopupButton with a text background color before asking + * HIToolbox to draw it. For PushButtons, we simply draw them in the + * active state. + */ + + BEGIN_DRAWING(d) + if (info.kind == kThemePopupButton && + (state & TTK_STATE_BACKGROUND)) { + CGRect innerBounds = CGRectInset(bounds, 1, 1); + FillRoundedRectangle(dc.context, innerBounds, 4, CG_WHITE); + } + + /* + * A BevelButton with mixed value is drawn borderless, which does make + * much sense for us. + */ + + if (info.kind == kThemeRoundedBevelButton && + info.value == kThemeButtonMixed) { + info.value = kThemeButtonOff; + info.state = kThemeStateInactive; } + if (info.kind == kThemePushButton) { + bounds.origin.y -= 2; + } + ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, + NULL); END_DRAWING } @@ -1555,7 +1854,7 @@ static Ttk_ElementSpec ButtonElementSpec = { */ /* Tab position logic, c.f. ttkNotebook.c TabState() */ -static Ttk_StateTable TabStyleTable[] = { +static const Ttk_StateTable TabStyleTable[] = { {kThemeTabFrontInactive, TTK_STATE_SELECTED | TTK_STATE_BACKGROUND, 0}, {kThemeTabNonFrontInactive, TTK_STATE_BACKGROUND, 0}, {kThemeTabFrontUnavailable, TTK_STATE_DISABLED | TTK_STATE_SELECTED, 0}, @@ -1564,13 +1863,13 @@ static Ttk_StateTable TabStyleTable[] = { {kThemeTabNonFrontPressed, TTK_STATE_PRESSED, 0}, {kThemeTabNonFront, 0, 0} }; -static Ttk_StateTable TabAdornmentTable[] = { +static const Ttk_StateTable TabAdornmentTable[] = { {kHIThemeTabAdornmentNone, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB, 0}, {kHIThemeTabAdornmentTrailingSeparator, TTK_STATE_FIRST_TAB, 0}, {kHIThemeTabAdornmentNone, TTK_STATE_LAST_TAB, 0}, {kHIThemeTabAdornmentTrailingSeparator, 0, 0}, }; -static Ttk_StateTable TabPositionTable[] = { +static const Ttk_StateTable TabPositionTable[] = { {kHIThemeTabPositionOnly, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB, 0}, {kHIThemeTabPositionFirst, TTK_STATE_FIRST_TAB, 0}, {kHIThemeTabPositionLast, TTK_STATE_LAST_TAB, 0}, @@ -1618,41 +1917,40 @@ static Ttk_StateTable TabPositionTable[] = { */ static void TabElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), - TCL_UNUSED(int *), - int *minHeight, + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ + TCL_UNUSED(int *), /* minWidth */ + TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { - GetThemeMetric(kThemeMetricLargeTabHeight, (SInt32 *) minHeight); - *paddingPtr = Ttk_MakePadding(0, 0, 0, 2); - + *paddingPtr = Ttk_MakePadding(0, -2, 0, 1); } static void TabElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, b); - HIThemeTabDrawInfo info = { - .version = 1, - .style = Ttk_StateTableLookup(TabStyleTable, state), - .direction = kThemeTabNorth, - .size = kHIThemeTabSizeNormal, - .adornment = Ttk_StateTableLookup(TabAdornmentTable, state), - .kind = kHIThemeTabKindNormal, - .position = Ttk_StateTableLookup(TabPositionTable, state), - }; - BEGIN_DRAWING(d) - if (TkMacOSXInDarkMode(tkwin)) { - DrawDarkTab(bounds, state, dc.context); + if ([NSApp macOSVersion] >= 110000) { + DrawTab11(bounds, state, dc.context, tkwin); + } else if ([NSApp macOSVersion] > 100800) { + DrawTab(bounds, state, dc.context, tkwin); } else { + HIThemeTabDrawInfo info = { + .version = 1, + .style = Ttk_StateTableLookup(TabStyleTable, state), + .direction = kThemeTabNorth, + .size = kHIThemeTabSizeNormal, + .adornment = Ttk_StateTableLookup(TabAdornmentTable, state), + .kind = kHIThemeTabKindNormal, + .position = Ttk_StateTableLookup(TabPositionTable, state), + }; ChkErr(HIThemeDrawTab, &bounds, &info, dc.context, HIOrientation, NULL); } @@ -1672,19 +1970,19 @@ static Ttk_ElementSpec TabElementSpec = { */ static void PaneElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), - TCL_UNUSED(int *), - TCL_UNUSED(int *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ + TCL_UNUSED(int *), /* minWidth */ + TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { *paddingPtr = Ttk_MakePadding(9, 5, 9, 9); } static void PaneElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -1696,7 +1994,7 @@ static void PaneElementDraw( bounds.size.height += kThemeMetricTabFrameOverlap; BEGIN_DRAWING(d) if ([NSApp macOSVersion] > 100800) { - DrawGroupBox(bounds, dc.context, tkwin); + DrawGroupBox(bounds, dc.context, tkwin, 1, YES); } else { HIThemeTabPaneDrawInfo info = { .version = 1, @@ -1728,24 +2026,22 @@ static Ttk_ElementSpec PaneElementSpec = { * DrawThemePrimaryGroup reference: "The primary group box frame is drawn * inside the specified rectangle and is a maximum of 2 pixels thick." * - * "Maximum of 2 pixels thick" is apparently a lie; looks more like 4 to me - * with shading. */ static void GroupElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), - TCL_UNUSED(int *), - TCL_UNUSED(int *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ + TCL_UNUSED(int *), /* minWidth */ + TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { - *paddingPtr = Ttk_UniformPadding(4); + *paddingPtr = Ttk_MakePadding(0, 0, 0, 0); } static void GroupElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -1755,7 +2051,7 @@ static void GroupElementDraw( BEGIN_DRAWING(d) if ([NSApp macOSVersion] > 100800) { - DrawGroupBox(bounds, dc.context, tkwin); + DrawGroupBox(bounds, dc.context, tkwin, 1, YES); } else { const HIThemeGroupBoxDrawInfo info = { .version = 0, @@ -1798,18 +2094,18 @@ static Ttk_ElementOptionSpec EntryElementOptions[] = { }; static void EntryElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), - TCL_UNUSED(int *), - TCL_UNUSED(int *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ + TCL_UNUSED(int *), /* minWidth */ + TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { - *paddingPtr = Ttk_MakePadding(7, 5, 7, 6); + *paddingPtr = entryElementPadding; } static void EntryElementDraw( - TCL_UNUSED(void *), + void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, @@ -1817,40 +2113,31 @@ static void EntryElementDraw( Ttk_State state) { EntryElement *e = (EntryElement *)elementRecord; - Ttk_Box inner = Ttk_PadBox(b, Ttk_UniformPadding(3)); - CGRect bounds = BoxToRect(d, inner); - NSColor *background; + ThemeFrameParams *params = (ThemeFrameParams *)clientData; + HIThemeFrameKind kind = params ? params->kind : + kHIThemeFrameTextFieldSquare; + CGRect bounds = BoxToRect(d, b); + CGColorRef background; Tk_3DBorder backgroundPtr = NULL; static const char *defaultBG = ENTRY_DEFAULT_BACKGROUND; - if (TkMacOSXInDarkMode(tkwin)) { + if ([NSApp macOSVersion] > 100800) { BEGIN_DRAWING(d) - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - CGFloat fill[4]; - GetBackgroundColor(dc.context, tkwin, 1, fill); - - /* - * Lighten the background to provide contrast. - */ - - for (int i = 0; i < 3; i++) { - fill[i] += 9.0 / 255.0; + switch(kind) { + case kHIThemeFrameTextFieldRound: + DrawEntry(dc.context, bounds, &searchDesign, state, tkwin); + break; + case kHIThemeFrameTextFieldSquare: + DrawEntry(dc.context, bounds, &entryDesign, state, tkwin); + break; + default: + return; } - background = [NSColor colorWithColorSpace: deviceRGB - components: fill - count: 4]; - CGContextSetFillColorWithColor(dc.context, CGCOLOR(background)); - CGContextFillRect(dc.context, bounds); - if (state & TTK_STATE_FOCUS) { - DrawDarkFocusRing(bounds, dc.context); - } else { - DrawDarkFrame(bounds, dc.context, kHIThemeFrameTextFieldSquare); - } END_DRAWING } else { const HIThemeFrameDrawInfo info = { .version = 0, - .kind = kHIThemeFrameTextFieldSquare, + .kind = params->kind, .state = Ttk_StateTableLookup(ThemeStateTable, state), .isFocused = state & TTK_STATE_FOCUS, }; @@ -1873,13 +2160,13 @@ static void EntryElementDraw( if (backgroundPtr != NULL) { XFillRectangle(Tk_Display(tkwin), d, Tk_3DBorderGC(tkwin, backgroundPtr, TK_3D_FLAT_GC), - inner.x, inner.y, inner.width, inner.height); + b.x, b.y, b.width, b.height); } BEGIN_DRAWING(d) if (backgroundPtr == NULL) { if ([NSApp macOSVersion] > 100800) { - background = [NSColor textBackgroundColor]; - CGContextSetFillColorWithColor(dc.context, CGCOLOR(background)); + background = CGCOLOR([NSColor textBackgroundColor]); + CGContextSetFillColorWithColor(dc.context, background); } else { CGContextSetRGBFillColor(dc.context, 1.0, 1.0, 1.0, 1.0); } @@ -1918,29 +2205,25 @@ static Ttk_ElementSpec EntryElementSpec = { * 1 pixel to account for the fact that the button is not centered. */ -static Ttk_Padding ComboboxPadding = {4, 4, 20, 4}; -static Ttk_Padding DarkComboboxPadding = {6, 6, 22, 6}; +// OS dependent ??? +static Ttk_Padding ComboboxPadding = {7, 5, 24, 5}; static void ComboboxElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - Tk_Window tkwin, + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, Ttk_Padding *paddingPtr) { *minWidth = 24; - *minHeight = 23; - if (TkMacOSXInDarkMode(tkwin)) { - *paddingPtr = DarkComboboxPadding; - } else { - *paddingPtr = ComboboxPadding; - } + *minHeight = 0; + *paddingPtr = ComboboxPadding; } static void ComboboxElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -1956,23 +2239,13 @@ static void ComboboxElementDraw( }; BEGIN_DRAWING(d) - if (TkMacOSXInDarkMode(tkwin)) { - bounds = CGRectInset(bounds, 3, 3); - if (state & TTK_STATE_FOCUS) { - DrawDarkFocusRing(bounds, dc.context); - } - DrawDarkButton(bounds, info.kind, state, dc.context); + if ([NSApp macOSVersion] > 100800) { + bounds = CGRectInset(bounds, -1, -1); + DrawButton(bounds, info, state, dc.context, tkwin); } else { - if ([NSApp macOSVersion] > 100800) { - if ((state & TTK_STATE_BACKGROUND) && - !(state & TTK_STATE_DISABLED)) { - NSColor *background = [NSColor textBackgroundColor]; - CGRect innerBounds = CGRectInset(bounds, 1, 4); - bounds.origin.y += 1; - SolidFillRoundedRectangle(dc.context, innerBounds, 4, background); - } - } - ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); + bounds.origin.y += 1; + ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, + NULL); } END_DRAWING } @@ -2006,27 +2279,42 @@ static Ttk_ElementSpec ComboboxElementSpec = { * that order. So the up button must be listed first in the layout. */ -static Ttk_Padding SpinbuttonMargins = {0, 0, 2, 0}; +static Ttk_Padding SpinbuttonMargins = {2, 0, 0, 0}; -static void SpinButtonUpElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), +static void SpinButtonReBounds( + Tk_Window tkwin, + CGRect *bounds) +{ + if (TkMacOSXInDarkMode(tkwin)) { + bounds->origin.x -= 2; + bounds->origin.y += 1; + bounds->size.height -= 0.5; + } else { + bounds->origin.x -= 3; + bounds->origin.y += 1; + bounds->size.width += 1; + } +} + +static void SpinButtonElementSize( + TCL_UNUSED(void *), /* clientdata */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* PaddingPtr */ { SInt32 s; ChkErr(GetThemeMetric, kThemeMetricLittleArrowsWidth, &s); *minWidth = s + Ttk_PaddingWidth(SpinbuttonMargins); ChkErr(GetThemeMetric, kThemeMetricLittleArrowsHeight, &s); - *minHeight = (s + Ttk_PaddingHeight(SpinbuttonMargins)) / 2; + *minHeight = 2 + (s + Ttk_PaddingHeight(SpinbuttonMargins)) / 2; } static void SpinButtonUpElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -2035,6 +2323,7 @@ static void SpinButtonUpElementDraw( CGRect bounds = BoxToRect(d, Ttk_PadBox(b, SpinbuttonMargins)); int infoState; + SpinButtonReBounds(tkwin, &bounds); bounds.size.height *= 2; if (state & TTK_STATE_PRESSED) { infoState = kThemeStatePressedUp; @@ -2049,8 +2338,8 @@ static void SpinButtonUpElementDraw( .adornment = kThemeAdornmentNone, }; BEGIN_DRAWING(d) - if (TkMacOSXInDarkMode(tkwin)) { - DrawDarkIncDecButton(bounds, infoState, state, dc.context); + if ([NSApp macOSVersion] > 100800) { + DrawButton(bounds, info, state, dc.context, tkwin); } else { ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); @@ -2062,28 +2351,13 @@ static Ttk_ElementSpec SpinButtonUpElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, - SpinButtonUpElementSize, + SpinButtonElementSize, SpinButtonUpElementDraw }; -static void SpinButtonDownElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), - int *minWidth, - int *minHeight, - TCL_UNUSED(Ttk_Padding *)) -{ - SInt32 s; - - ChkErr(GetThemeMetric, kThemeMetricLittleArrowsWidth, &s); - *minWidth = s + Ttk_PaddingWidth(SpinbuttonMargins); - ChkErr(GetThemeMetric, kThemeMetricLittleArrowsHeight, &s); - *minHeight = (s + Ttk_PaddingHeight(SpinbuttonMargins)) / 2; -} static void SpinButtonDownElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -2092,8 +2366,9 @@ static void SpinButtonDownElementDraw( CGRect bounds = BoxToRect(d, Ttk_PadBox(b, SpinbuttonMargins)); int infoState = 0; + SpinButtonReBounds(tkwin, &bounds); bounds.origin.y -= bounds.size.height; - bounds.size.height *= 2; + bounds.size.height += bounds.size.height; if (state & TTK_STATE_PRESSED) { infoState = kThemeStatePressedDown; } else { @@ -2108,8 +2383,8 @@ static void SpinButtonDownElementDraw( }; BEGIN_DRAWING(d) - if (TkMacOSXInDarkMode(tkwin)) { - DrawDarkIncDecButton(bounds, infoState, state, dc.context); + if ([NSApp macOSVersion] > 100800) { + DrawButton(bounds, info, state, dc.context, tkwin); } else { ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); @@ -2121,7 +2396,7 @@ static Ttk_ElementSpec SpinButtonDownElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, - SpinButtonDownElementSize, + SpinButtonElementSize, SpinButtonDownElementDraw }; @@ -2136,7 +2411,7 @@ static Ttk_ElementSpec SpinButtonDownElementSpec = { * inactive. So we shouldn't either. */ -static Ttk_StateTable ThemeTrackEnableTable[] = { +static const Ttk_StateTable ThemeTrackEnableTable[] = { {kThemeTrackDisabled, TTK_STATE_DISABLED, 0}, {kThemeTrackActive, TTK_STATE_BACKGROUND, 0}, {kThemeTrackActive, 0, 0} @@ -2168,11 +2443,11 @@ static Ttk_ElementOptionSpec TrackElementOptions[] = { }; static void TrackElementSize( void *clientData, - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { TrackElementData *data = (TrackElementData *)clientData; SInt32 size = 24; /* reasonable default ... */ @@ -2192,33 +2467,28 @@ static void TrackElementDraw( TrackElementData *data = (TrackElementData *)clientData; TrackElement *elem = (TrackElement *)elementRecord; Ttk_Orient orientation = TTK_ORIENT_HORIZONTAL; - double from = 0, to = 100, value = 0, factor; - CGRect bounds; + double from = 0, to = 100, value = 0, fraction, max; + CGRect bounds = BoxToRect(d, b); TtkGetOrientFromObj(NULL, elem->orientObj, &orientation); Tcl_GetDoubleFromObj(NULL, elem->fromObj, &from); Tcl_GetDoubleFromObj(NULL, elem->toObj, &to); Tcl_GetDoubleFromObj(NULL, elem->valueObj, &value); - factor = RangeToFactor(to); - - /* - * HIThemeTrackDrawInfo uses 2-byte alignment; assigning to a separate - * bounds variable avoids UBSan (-fsanitize=alignment) complaints. - */ - bounds = BoxToRect(d, b); + fraction = (value - from) / (to - from); + max = RangeToFactor(fabs(to - from)); HIThemeTrackDrawInfo info = { .version = 0, .kind = data->kind, .bounds = bounds, - .min = from * factor, - .max = to * factor, - .value = value * factor, + .min = 0, + .max = max, + .value = fraction * max, .attributes = kThemeTrackShowThumb | (orientation == TTK_ORIENT_HORIZONTAL ? kThemeTrackHorizontal : 0), .enableState = Ttk_StateTableLookup(ThemeTrackEnableTable, state), - .trackInfo.progress.phase = 0, + .trackInfo.progress.phase = 0 }; if (info.kind == kThemeSlider) { @@ -2231,20 +2501,19 @@ static void TrackElementDraw( } } BEGIN_DRAWING(d) - if (TkMacOSXInDarkMode(tkwin)) { - bounds = BoxToRect(d, b); - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *trackColor = [NSColor colorWithColorSpace: deviceRGB - components: darkTrack - count: 4]; - if (orientation == TTK_ORIENT_HORIZONTAL) { - bounds = CGRectInset(bounds, 1, bounds.size.height / 2 - 2); - } else { - bounds = CGRectInset(bounds, bounds.size.width / 2 - 3, 2); - } - SolidFillRoundedRectangle(dc.context, bounds, 2, trackColor); + if (([NSApp macOSVersion] > 100800) && !(state & TTK_STATE_ALTERNATE)) { + + /* + * We use the reserved field to indicate whether "from" is less than + * "to". It should be 0 if passing the info to HIThemeDrawInfo, but + * we aren't doing that. + */ + + info.reserved = (from < to); + DrawSlider(dc.context, bounds, info, state, tkwin); + } else { + ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); } - ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); END_DRAWING } @@ -2266,12 +2535,12 @@ static Ttk_ElementSpec TrackElementSpec = { */ static void SliderElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { *minWidth = *minHeight = 24; } @@ -2314,12 +2583,12 @@ static Ttk_ElementOptionSpec PbarElementOptions[] = { {NULL, TK_OPTION_BOOLEAN, 0, NULL} }; static void PbarElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { SInt32 size = 24; /* @@@ Check HIG for correct default */ @@ -2328,7 +2597,7 @@ static void PbarElementSize( } static void PbarElementDraw( - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ void *elementRecord, Tk_Window tkwin, Drawable d, @@ -2337,70 +2606,50 @@ static void PbarElementDraw( { PbarElement *pbar = (PbarElement *)elementRecord; Ttk_Orient orientation = TTK_ORIENT_HORIZONTAL; - - /* - * Using 1000 as the maximum should give better than 1 pixel - * resolution for most progress bars. - */ - - int kind, phase = 0, ivalue, imaximum = 1000; - CGRect bounds; + int phase; + double value = 0, maximum = 100, factor; + CGRect bounds = BoxToRect(d, b); + int isIndeterminate = !strcmp("indeterminate", + Tcl_GetString(pbar->modeObj)); TtkGetOrientFromObj(NULL, pbar->orientObj, &orientation); - kind = !strcmp("indeterminate", Tcl_GetString(pbar->modeObj)) ? - kThemeIndeterminateBar : kThemeProgressBar; - if (kind == kThemeIndeterminateBar) { - Tcl_GetIntFromObj(NULL, pbar->phaseObj, &phase); + Tcl_GetDoubleFromObj(NULL, pbar->valueObj, &value); + Tcl_GetDoubleFromObj(NULL, pbar->maximumObj, &maximum); + Tcl_GetIntFromObj(NULL, pbar->phaseObj, &phase); + + if (isIndeterminate) { /* - * On macOS 11 the fraction of an indeterminate progress bar which is - * traversed by the oscillating thumb is value / maximum. The phase - * determines the position of the moving thumb in that range and is - * apparently expected to vary between 0 and 120. On earlier systems - * it is unclear how the phase is used in generating the animation. + * When an indeterminate progress bar is animated the phase is + * (currently) always 0 and the value increases from min to max + * and then decreases back to min. We scale the value by 3 to + * speed the animation up a bit. */ - ivalue = imaximum; - } else { - double value, maximum; - Tcl_GetDoubleFromObj(NULL, pbar->valueObj, &value); - Tcl_GetDoubleFromObj(NULL, pbar->maximumObj, &maximum); - ivalue = (value / maximum)*1000; + double remainder = fmod(3*value, 2*maximum); + value = remainder > maximum ? 2*maximum - remainder : remainder; } - - /* - * HIThemeTrackDrawInfo uses 2-byte alignment; assigning to a separate - * bounds variable avoids UBSan (-fsanitize=alignment) complaints. - */ - - bounds = BoxToRect(d, b); + factor = RangeToFactor(maximum); HIThemeTrackDrawInfo info = { .version = 0, - .kind = kind, + .kind = isIndeterminate? kThemeIndeterminateBar : kThemeProgressBar, .bounds = bounds, .min = 0, - .max = imaximum, - .value = ivalue, + .max = maximum * factor, + .value = value * factor, .attributes = kThemeTrackShowThumb | - (orientation == TTK_ORIENT_HORIZONTAL ? kThemeTrackHorizontal : 0), + (orientation == TTK_ORIENT_HORIZONTAL ? + kThemeTrackHorizontal : 0), .enableState = Ttk_StateTableLookup(ThemeTrackEnableTable, state), - .trackInfo.progress.phase = phase + .trackInfo.progress.phase = phase, }; + BEGIN_DRAWING(d) - if (TkMacOSXInDarkMode(tkwin)) { - bounds = BoxToRect(d, b); - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *trackColor = [NSColor colorWithColorSpace: deviceRGB - components: darkTrack - count: 4]; - if (orientation == TTK_ORIENT_HORIZONTAL) { - bounds = CGRectInset(bounds, 1, bounds.size.height / 2 - 3); - } else { - bounds = CGRectInset(bounds, bounds.size.width / 2 - 3, 1); - } - SolidFillRoundedRectangle(dc.context, bounds, 3, trackColor); + if ([NSApp macOSVersion] > 100800) { + DrawProgressBar(dc.context, bounds, info, state, tkwin); + } else { + ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); } - ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); END_DRAWING } @@ -2427,9 +2676,9 @@ static Ttk_ElementOptionSpec ScrollbarElementOptions[] = { {NULL, TK_OPTION_BOOLEAN, 0, NULL} }; static void TroughElementSize( - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ void *elementRecord, - TCL_UNUSED(Tk_Window), + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, Ttk_Padding *paddingPtr) @@ -2453,34 +2702,18 @@ static void TroughElementSize( } } -static CGFloat lightTrough[4] = {250.0 / 255, 250.0 / 255, 250.0 / 255, 1.0}; -static CGFloat darkTrough[4] = {45.0 / 255, 46.0 / 255, 49.0 / 255, 1.0}; -static CGFloat lightInactiveThumb[4] = { - 200.0 / 255, 200.0 / 255, 200.0 / 255, 1.0 -}; -static CGFloat lightActiveThumb[4] = { - 133.0 / 255, 133.0 / 255, 133.0 / 255, 1.0 -}; -static CGFloat darkInactiveThumb[4] = { - 116.0 / 255, 117.0 / 255, 118.0 / 255, 1.0 -}; -static CGFloat darkActiveThumb[4] = { - 158.0 / 255, 158.0 / 255, 159.0 / 255, 1.0 -}; static void TroughElementDraw( - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, - TCL_UNUSED(Ttk_State)) + TCL_UNUSED(Ttk_State)) /* state */ { ScrollbarElement *scrollbar = (ScrollbarElement *)elementRecord; Ttk_Orient orientation = TTK_ORIENT_HORIZONTAL; CGRect bounds = BoxToRect(d, b); - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *troughColor; - CGFloat *rgba = TkMacOSXInDarkMode(tkwin) ? darkTrough : lightTrough; + GrayColor bgGray; TtkGetOrientFromObj(NULL, scrollbar->orientObj, &orientation); if (orientation == TTK_ORIENT_HORIZONTAL) { @@ -2488,12 +2721,10 @@ static void TroughElementDraw( } else { bounds = CGRectInset(bounds, 1, 0); } - troughColor = [NSColor colorWithColorSpace: deviceRGB - components: rgba - count: 4]; BEGIN_DRAWING(d) if ([NSApp macOSVersion] > 100800) { - CGContextSetFillColorWithColor(dc.context, CGCOLOR(troughColor)); + bgGray = TkMacOSXInDarkMode(tkwin) ? darkTrough : lightTrough; + CGContextSetFillColorWithColor(dc.context, CGColorFromGray(bgGray)); } else { ChkErr(HIThemeSetFill, kThemeBrushDocumentWindowBackground, NULL, dc.context, HIOrientation); @@ -2510,12 +2741,12 @@ static Ttk_ElementSpec TroughElementSpec = { TroughElementDraw }; static void ThumbElementSize( - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ void *elementRecord, - TCL_UNUSED(Tk_Window), + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { ScrollbarElement *scrollbar = (ScrollbarElement *)elementRecord; Ttk_Orient orientation = TTK_ORIENT_HORIZONTAL; @@ -2531,7 +2762,7 @@ static void ThumbElementSize( } static void ThumbElementDraw( - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ void *elementRecord, Tk_Window tkwin, Drawable d, @@ -2550,16 +2781,20 @@ static void ThumbElementDraw( * determine the thumb geometry from the input values of min, max, value * and viewSize is undocumented. A seemingly natural algorithm is * implemented below. This code uses that algorithm for older OS versions, - * because using HITools also handles drawing the buttons and 3D thumb used + * because using HIToolbox also handles drawing the buttons and 3D thumb used * on those systems. For newer systems the cleanest approach is to just * draw the thumb directly. */ if ([NSApp macOSVersion] > 100800) { CGRect thumbBounds = BoxToRect(d, b); - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *thumbColor; - CGFloat *rgba; + CGColorRef thumbColor; + GrayColor bgGray; + + /* + * Apple does not draw the thumb when scrolling is not possible. + */ + if ((orientation == TTK_ORIENT_HORIZONTAL && thumbBounds.size.width >= Tk_Width(tkwin) - 8) || (orientation == TTK_ORIENT_VERTICAL && @@ -2569,15 +2804,13 @@ static void ThumbElementDraw( int isDark = TkMacOSXInDarkMode(tkwin); if ((state & TTK_STATE_PRESSED) || (state & TTK_STATE_HOVER)) { - rgba = isDark ? darkActiveThumb : lightActiveThumb; + bgGray = isDark ? darkActiveThumb : lightActiveThumb; } else { - rgba = isDark ? darkInactiveThumb : lightInactiveThumb; + bgGray = isDark ? darkInactiveThumb : lightInactiveThumb; } - thumbColor = [NSColor colorWithColorSpace: deviceRGB - components: rgba - count: 4]; + thumbColor = CGColorFromGray(bgGray); BEGIN_DRAWING(d) - SolidFillRoundedRectangle(dc.context, thumbBounds, 4, thumbColor); + FillRoundedRectangle(dc.context, thumbBounds, 4, thumbColor); END_DRAWING } else { double thumbSize, trackSize, visibleSize, factor, fraction; @@ -2646,12 +2879,12 @@ static Ttk_ElementSpec ThumbElementSpec = { ThumbElementDraw }; static void ArrowElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { if ([NSApp macOSVersion] < 100800) { *minHeight = *minWidth = 14; @@ -2677,19 +2910,19 @@ static Ttk_ElementSpec ArrowElementSpec = { */ static void SeparatorElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { *minWidth = *minHeight = 1; } static void SeparatorElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -2729,12 +2962,12 @@ static const ThemeGrowDirection sizegripGrowDirection = kThemeGrowRight | kThemeGrowDown; static void SizegripElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { HIThemeGrowBoxDrawInfo info = { .version = 0, @@ -2751,9 +2984,9 @@ static void SizegripElementSize( } static void SizegripElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ Drawable d, Ttk_Box b, unsigned int state) @@ -2828,8 +3061,8 @@ static Ttk_ElementSpec SizegripElementSpec = { */ static void FillElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -2838,15 +3071,10 @@ static void FillElementDraw( CGRect bounds = BoxToRect(d, b); if ([NSApp macOSVersion] > 100800) { - NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; - NSColor *bgColor; - CGFloat fill[4]; + CGColorRef bgColor; BEGIN_DRAWING(d) - GetBackgroundColor(dc.context, tkwin, 0, fill); - bgColor = [NSColor colorWithColorSpace: deviceRGB components: fill - count: 4]; - CGContextSetFillColorSpace(dc.context, deviceRGB.CGColorSpace); - CGContextSetFillColorWithColor(dc.context, CGCOLOR(bgColor)); + bgColor = GetBackgroundCGColor(dc.context, tkwin, NO, 0); + CGContextSetFillColorWithColor(dc.context, bgColor); CGContextFillRect(dc.context, bounds); END_DRAWING } else { @@ -2855,7 +3083,6 @@ static void FillElementDraw( : kThemeBrushModelessDialogBackgroundActive; BEGIN_DRAWING(d) ChkErr(HIThemeSetFill, brush, NULL, dc.context, HIOrientation); - //QDSetPatternOrigin(PatternOrigin(tkwin, d)); CGContextFillRect(dc.context, bounds); END_DRAWING } @@ -2903,8 +3130,8 @@ static Ttk_ElementSpec BackgroundElementSpec = { */ static void ToolbarBackgroundElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, TCL_UNUSED(Ttk_Box), @@ -2946,7 +3173,7 @@ static Ttk_ElementOptionSpec FieldElementOptions[] = { }; static void FieldElementDraw( - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ void *elementRecord, Tk_Window tkwin, Drawable d, @@ -2973,18 +3200,18 @@ static Ttk_ElementSpec FieldElementSpec = { /*---------------------------------------------------------------------- * +++ Treeview headers -- * - * On systems older than 10.9 The header is a kThemeListHeaderButton drawn + * On systems older than 10.9 the header is a kThemeListHeaderButton drawn * by HIToolbox. On newer systems those buttons do not match the Apple * buttons, so we draw them from scratch. */ -static Ttk_StateTable TreeHeaderValueTable[] = { +static const Ttk_StateTable TreeHeaderValueTable[] = { {kThemeButtonOn, TTK_STATE_ALTERNATE, 0}, {kThemeButtonOn, TTK_STATE_SELECTED, 0}, {kThemeButtonOff, 0, 0} }; -static Ttk_StateTable TreeHeaderAdornmentTable[] = { +static const Ttk_StateTable TreeHeaderAdornmentTable[] = { {kThemeAdornmentHeaderButtonSortUp, TTK_STATE_ALTERNATE | TTK_TREEVIEW_STATE_SORTARROW, 0}, {kThemeAdornmentDefault, @@ -2996,11 +3223,11 @@ static Ttk_StateTable TreeHeaderAdornmentTable[] = { }; static void TreeAreaElementSize ( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), - TCL_UNUSED(int *), - TCL_UNUSED(int *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ + TCL_UNUSED(int *), /* minWidth */ + TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { @@ -3010,7 +3237,7 @@ static void TreeAreaElementSize ( */ if ([NSApp macOSVersion] > 100800) { - paddingPtr->top = 4; + *paddingPtr = Ttk_MakePadding(0, 4, 0, 0); } } @@ -3039,7 +3266,7 @@ static void TreeHeaderElementSize( static void TreeHeaderElementDraw( void *clientData, - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -3064,11 +3291,7 @@ static void TreeHeaderElementDraw( */ bounds.origin.y -= 4; - if (TkMacOSXInDarkMode(tkwin)) { - DrawDarkListHeader(bounds, dc.context, tkwin, state); - } else { - DrawListHeader(bounds, dc.context, tkwin, state); - } + DrawListHeader(bounds, dc.context, tkwin, state); } else { ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); @@ -3090,17 +3313,17 @@ static Ttk_ElementSpec TreeHeaderElementSpec = { #define TTK_TREEVIEW_STATE_OPEN TTK_STATE_USER1 #define TTK_TREEVIEW_STATE_LEAF TTK_STATE_USER2 -static Ttk_StateTable DisclosureValueTable[] = { +static const Ttk_StateTable DisclosureValueTable[] = { {kThemeDisclosureDown, TTK_TREEVIEW_STATE_OPEN, 0}, {kThemeDisclosureRight, 0, 0}, }; static void DisclosureElementSize( - TCL_UNUSED(void *), - TCL_UNUSED(void *), - TCL_UNUSED(Tk_Window), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ + TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, - TCL_UNUSED(Ttk_Padding *)) + TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { SInt32 s; @@ -3111,8 +3334,8 @@ static void DisclosureElementSize( } static void DisclosureElementDraw( - TCL_UNUSED(void *), - TCL_UNUSED(void *), + TCL_UNUSED(void *), /* clientData */ + TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, @@ -3192,6 +3415,44 @@ TTK_LAYOUT("TCombobox", TTK_GROUP("Combobox.padding", TTK_FILL_BOTH, TTK_NODE("Combobox.textarea", TTK_FILL_BOTH)))) +/* Image Button - no button */ +TTK_LAYOUT("ImageButton", + TTK_GROUP("Button.padding", TTK_FILL_BOTH, + TTK_NODE("Button.label", TTK_FILL_BOTH))) + +/* Inline Button */ +TTK_LAYOUT("InlineButton", + TTK_GROUP("InlineButton.button", TTK_FILL_BOTH, + TTK_GROUP("Button.padding", TTK_FILL_BOTH, + TTK_NODE("Button.label", TTK_FILL_BOTH)))) + +/* Rounded Rect Button -- transparent face */ +TTK_LAYOUT("RoundedRectButton", + TTK_GROUP("RoundedRectButton.button", TTK_FILL_BOTH, + TTK_GROUP("Button.padding", TTK_FILL_BOTH, + TTK_NODE("Button.label", TTK_FILL_BOTH)))) + +/* Gradient Button */ +TTK_LAYOUT("GradientButton", + TTK_GROUP("GradientButton.button", TTK_FILL_BOTH, + TTK_GROUP("Button.padding", TTK_FILL_BOTH, + TTK_NODE("Button.label", TTK_FILL_BOTH)))) + +/* Recessed Button - text only radio button */ + +TTK_LAYOUT("RecessedButton", + TTK_GROUP("RecessedButton.button", TTK_FILL_BOTH, + TTK_GROUP("Button.padding", TTK_FILL_BOTH, + TTK_NODE("Button.label", TTK_FILL_BOTH)))) + +/* DisclosureButton (not a triangle) -- No label, no border*/ +TTK_LAYOUT("DisclosureButton", + TTK_NODE("DisclosureButton.button", TTK_FILL_BOTH)) + +/* HelpButton -- No label, no border*/ +TTK_LAYOUT("HelpButton", + TTK_NODE("HelpButton.button", TTK_FILL_BOTH)) + /* Notebook tabs -- no focus ring */ TTK_LAYOUT("Tab", TTK_GROUP("Notebook.tab", TTK_FILL_BOTH, @@ -3206,6 +3467,17 @@ TTK_LAYOUT("TSpinbox", TTK_GROUP("Spinbox.field", TTK_FILL_X, TTK_NODE("Spinbox.textarea", TTK_FILL_X))) +TTK_LAYOUT("TEntry", + TTK_GROUP("Entry.field", TTK_FILL_BOTH|TTK_BORDER, + TTK_GROUP("Entry.padding", TTK_FILL_BOTH, + TTK_NODE("Entry.textarea", TTK_FILL_BOTH)))) + +/* Searchbox */ +TTK_LAYOUT("Searchbox", + TTK_GROUP("Searchbox.field", TTK_FILL_BOTH|TTK_BORDER, + TTK_GROUP("Entry.padding", TTK_FILL_BOTH, + TTK_NODE("Entry.textarea", TTK_FILL_BOTH)))) + /* Progress bars -- track only */ TTK_LAYOUT("TProgressbar", TTK_NODE("Progressbar.track", TTK_FILL_BOTH)) @@ -3249,6 +3521,34 @@ TTK_END_LAYOUT_TABLE * +++ Initialization -- */ +/*---------------------------------------------------------------------- + * +++ Ttk_MacOSXInit -- + * + * Initialize variables which depend on [NSApp macOSVersion]. Called from + * [NSApp applicationDidFinishLaunching]. + */ + +MODULE_SCOPE +void Ttk_MacOSXInit( + void) +{ + if ([NSApp macOSVersion] < 101400) { + entryElementPadding = Ttk_MakePadding(7, 6, 7, 5); + } else { + entryElementPadding = Ttk_MakePadding(7, 5, 7, 6); + } + if ([NSApp macOSVersion] < 110000) { + Ttk_ContrastDelta = 8.0; + } else { + + /* + * The subtle contrast became event more subtle in 11.0. + */ + + Ttk_ContrastDelta = 5.0; + } +} + static int AquaTheme_Init( Tcl_Interp *interp) { @@ -3271,14 +3571,26 @@ static int AquaTheme_Init( Ttk_RegisterElementSpec(themePtr, "Button.button", &ButtonElementSpec, &PushButtonParams); + Ttk_RegisterElementSpec(themePtr, "InlineButton.button", + &ButtonElementSpec, &InlineButtonParams); + Ttk_RegisterElementSpec(themePtr, "RoundedRectButton.button", + &ButtonElementSpec, &RoundedRectButtonParams); Ttk_RegisterElementSpec(themePtr, "Checkbutton.button", &ButtonElementSpec, &CheckBoxParams); Ttk_RegisterElementSpec(themePtr, "Radiobutton.button", &ButtonElementSpec, &RadioButtonParams); + Ttk_RegisterElementSpec(themePtr, "RecessedButton.button", + &ButtonElementSpec, &RecessedButtonParams); Ttk_RegisterElementSpec(themePtr, "Toolbutton.border", &ButtonElementSpec, &BevelButtonParams); Ttk_RegisterElementSpec(themePtr, "Menubutton.button", &ButtonElementSpec, &PopupButtonParams); + Ttk_RegisterElementSpec(themePtr, "DisclosureButton.button", + &ButtonElementSpec, &DisclosureButtonParams); + Ttk_RegisterElementSpec(themePtr, "HelpButton.button", + &ButtonElementSpec, &HelpButtonParams); + Ttk_RegisterElementSpec(themePtr, "GradientButton.button", + &ButtonElementSpec, &GradientButtonParams); Ttk_RegisterElementSpec(themePtr, "Spinbox.uparrow", &SpinButtonUpElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Spinbox.downarrow", @@ -3297,8 +3609,12 @@ static int AquaTheme_Init( Ttk_RegisterElementSpec(themePtr, "Labelframe.border", &GroupElementSpec, 0); - Ttk_RegisterElementSpec(themePtr, "Entry.field", &EntryElementSpec, 0); - Ttk_RegisterElementSpec(themePtr, "Spinbox.field", &EntryElementSpec, 0); + Ttk_RegisterElementSpec(themePtr, "Entry.field", &EntryElementSpec, + &EntryFieldParams); + Ttk_RegisterElementSpec(themePtr, "Searchbox.field", &EntryElementSpec, + &SearchboxFieldParams); + Ttk_RegisterElementSpec(themePtr, "Spinbox.field", &EntryElementSpec, + &EntryFieldParams); Ttk_RegisterElementSpec(themePtr, "separator", &SeparatorElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "hseparator", &SeparatorElementSpec, 0); diff --git a/macosx/ttkMacOSXTheme.h b/macosx/ttkMacOSXTheme.h new file mode 100644 index 0000000..977c168 --- /dev/null +++ b/macosx/ttkMacOSXTheme.h @@ -0,0 +1,563 @@ +/* + * ttkMacOSXTheme.h -- + * + * Static data and macros used in ttkMacOSXTheme.c + * + * Copyright 2019 Marc Culler + * + * See the file "license.terms" for information on usage and redistribution + * of this file, and for a DISCLAIMER OF ALL WARRANTIES. + */ + +/* + * Macros for handling drawing contexts. + */ + +#define BEGIN_DRAWING(d) { \ + TkMacOSXDrawingContext dc; \ + if (!TkMacOSXSetupDrawingContext((d), NULL, &dc)) { \ + return; \ + } \ + +#define END_DRAWING \ + TkMacOSXRestoreDrawingContext(&dc);} + + +#define HIOrientation kHIThemeOrientationNormal +#define NoThemeMetric 0xFFFFFFFF + +/* + * A scale factor used to map a range of non-negative doubles into a large + * range of non-negative 32-bit integers without losing too much information. + * (Use 1073741824 == 2^30 so we don't get surprised by roundoff.) + */ + +#define RangeToFactor(max) ((double) (1073741824.0) / (max < 1.0 ? 1.0 : max)) + +/* + * Meanings of Ttk states represented by User1 and User2. + */ + +#define TTK_STATE_FIRST_TAB TTK_STATE_USER1 +#define TTK_STATE_LAST_TAB TTK_STATE_USER2 +#define TTK_STATE_IS_ACCENTED TTK_STATE_USER2 +#define TTK_TREEVIEW_STATE_SORTARROW TTK_STATE_USER1 + +/* + * Colors and gradients used when drawing buttons. + */ + +typedef struct GrayColor { + CGFloat grayscale; + CGFloat alpha; +} GrayColor; + +#define RGBACOLOR static CGFloat +#define RGBA256(r, g, b, a) {r / 255.0, g / 255.0, b / 255.0, a} +#define GRAYCOLOR static GrayColor +#define GRAY256(grayscale) {grayscale / 255.0, 1.0} + +/* + * Opaque Grays used for Gradient Buttons, Scrollbars and List Headers + */ + +GRAYCOLOR darkDisabledIndicator = GRAY256(122.0); +GRAYCOLOR lightDisabledIndicator = GRAY256(152.0); + +GRAYCOLOR darkGradientNormal = GRAY256(95.0); +GRAYCOLOR darkGradientPressed = GRAY256(118.0); +GRAYCOLOR darkGradientDisabled = GRAY256(82.0); +GRAYCOLOR darkGradientBorder = GRAY256(118.0); +GRAYCOLOR darkGradientBorderDisabled = GRAY256(94.0); +GRAYCOLOR lightGradientNormal = GRAY256(244.0); +GRAYCOLOR lightGradientPressed = GRAY256(175.0); +GRAYCOLOR lightGradientDisabled = GRAY256(235.0); +GRAYCOLOR lightGradientBorder = GRAY256(165.0); +GRAYCOLOR lightGradientBorderDisabled = GRAY256(204.0); + +GRAYCOLOR lightTrough = GRAY256(250.0); +GRAYCOLOR darkTrough = GRAY256(47.0); +GRAYCOLOR lightInactiveThumb = GRAY256(200.0); +GRAYCOLOR lightActiveThumb = GRAY256(133.0); +GRAYCOLOR darkInactiveThumb = GRAY256(117.0); +GRAYCOLOR darkActiveThumb = GRAY256(158.0); + +GRAYCOLOR lightListheaderBorder = GRAY256(200.0); +GRAYCOLOR listheaderSeparator = GRAY256(220.0); +GRAYCOLOR listheaderActiveBG = GRAY256(238.0); +GRAYCOLOR listheaderInactiveBG = GRAY256(246.0); + +GRAYCOLOR lightComboSeparator = GRAY256(236.0); +GRAYCOLOR darkComboSeparator = GRAY256(66.0); + +GRAYCOLOR darkTrack = GRAY256(84.0); +GRAYCOLOR darkInactiveTrack = GRAY256(107.0); +GRAYCOLOR lightTrack = GRAY256(177.0); +GRAYCOLOR lightInactiveTrack = GRAY256(139.0); + +/* + * Transparent Grays + */ + +GRAYCOLOR boxBorder = {1.0, 0.20}; +GRAYCOLOR darkSeparator = {1.0, 0.3}; +GRAYCOLOR darkTabSeparator = {0.0, 0.25}; +GRAYCOLOR darkListheaderBorder = {1.0, 0.125}; +GRAYCOLOR pressedDefaultButton = {0.0, 0.1}; +GRAYCOLOR darkPressedDefaultButton = {1.0, 0.1}; + +#define CG_WHITE CGColorGetConstantColor(kCGColorWhite) + + +/* + * Structures which comprise a database of corner radii and state-dependent + * colors used when drawing various types of buttons or entry widgets. + */ + +typedef struct GrayPalette { + CGFloat face; + CGFloat top; + CGFloat side; + CGFloat bottom; +} GrayPalette; + +typedef struct PaletteStateTable { + GrayPalette light; /* Light palette to use if this entry matches */ + GrayPalette dark; /* dark palette to use if this entry matches */ + unsigned int onBits; /* Bits which must be set */ + unsigned int offBits; /* Bits which must be cleared */ +} PaletteStateTable; + +typedef struct ButtonDesign { + CGFloat radius; + PaletteStateTable palettes[]; +} ButtonDesign; + +/* + * The data. + */ + +static const ButtonDesign pushbuttonDesign = { + .radius = 4.0, + .palettes = { + { + .light = {.face = 242.0, .top = 213.0, .side = 210.0, .bottom = 200.0}, + .dark = {.face = 94.0, .top = 98.0, .side = 94.0, .bottom = 58.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0}, + { + .light = {.face = 205.0, .top = 215.0, .side = 211.0, .bottom = 173.0}, + .dark = {.face = 140.0, .top = 150.0, .side = 140.0, .bottom = 42.0}, + .onBits = TTK_STATE_PRESSED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 198.0, .side = 192.0, .bottom = 173.0}, + .dark = {.face = 118.0, .top = 132.0, .side = 118.0, .bottom = 48.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign helpDesign = { + .radius = 11, + .palettes = { + { + .light = {.face = 241.0, .top = 218.0, .side = 217.0, .bottom = 206.0}, + .dark = {.face = 83.0, .top = 89.0, .side = 82.0, .bottom = 82.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0}, + { + .light = {.face = 240.0, .top = 208.0, .side = 205.0, .bottom = 188.0}, + .dark = {.face = 129.0, .top = 121.0, .side = 106.0, .bottom = 106.0}, + .onBits = TTK_STATE_PRESSED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 208.0, .side = 205.0, .bottom = 188.0}, + .dark = {.face = 107.0, .top = 121.0, .side = 106.0, .bottom = 106.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign inlineDesign = { + .radius = 8.0, + .palettes = { + { + .light = {.face = 170.0, .top = 170.0, .side = 170.0, .bottom = 170.0}, + .dark = {.face = 106.0, .top = 106.0, .side = 106.0, .bottom = 106.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0}, + { + .light = {.face = 34.0, .top = 34.0, .side = 34.0, .bottom = 34.0}, + .dark = {.face = 225.0, .top = 225.0, .side = 225.0, .bottom = 225.0}, + .onBits = TTK_STATE_PRESSED, .offBits = 0 + }, + { + .light = {.face = 113.0, .top = 113.0, .side = 113.0, .bottom = 113.0}, + .dark = {.face = 165.0, .top = 165.0, .side = 165.0, .bottom = 165.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign roundedrectDesign = { + .radius = 3.0, + .palettes = { + { + .light = {.face = 256.0, .top = 192.0, .side = 192.0, .bottom = 192.0}, + .dark = {.face = 256.0, .top = 165.0, .side = 163.0, .bottom = 42.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 204.0, .top = 158.0, .side = 158.0, .bottom = 158.0}, + .dark = {.face = 85.0, .top = 115.0, .side = 115.0, .bottom = 115.0}, + .onBits = TTK_STATE_PRESSED, .offBits = 0 + }, + { + .light = {.face = 205.0, .top = 215.0, .side = 211.0, .bottom = 173.0}, + .dark = {.face = 140.0, .top = 150.0, .side = 140.0, .bottom = 42.0}, + .onBits = TTK_STATE_ALTERNATE, .offBits = TTK_STATE_BACKGROUND + }, + + /* + * Gray values > 255 are replaced by the background color. + */ + + { + .light = {.face = 256.0, .top = 158.0, .side = 158.0, .bottom = 158.0}, + .dark = {.face = 256.0, .top = 115.0, .side = 115.0, .bottom = 115.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign popupDesign = { + .radius = 4.0, + .palettes = { + { + .light = {.face = 242.0, .top = 213.0, .side = 210.0, .bottom = 200.0}, + .dark = {.face = 94.0, .top = 98.0, .side = 94.0, .bottom = 58.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 198.0, .side = 192.0, .bottom = 173.0}, + .dark = {.face = 118.0, .top = 132.0, .side = 118.0, .bottom = 48.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign checkDesign = { + .radius = 4.0, + .palettes = { + { + .light = {.face = 242.0, .top = 192.0, .side = 199.0, .bottom = 199.0}, + .dark = {.face = 80.0, .top = 90.0, .side = 80.0, .bottom = 49.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 165.0, .side = 184.0, .bottom = 184.0}, + .dark = {.face = 118.0, .top = 132.0, .side = 118.0, .bottom = 48.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign radioDesign = { + .radius = 8.0, + .palettes = { + { + .light = {.face = 242.0, .top = 189.0, .side = 198.0, .bottom = 199.0}, + .dark = {.face = 80.0, .top = 84.0, .side = 88.0, .bottom = 60.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 165.0, .side = 184.0, .bottom = 184.0}, + .dark = {.face = 118.0, .top = 132.0, .side = 118.0, .bottom = 48.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign recessedDesign = { + .radius = 4.0, + .palettes = { + { + .light = {.face = 117.0, .top = 117.0, .side = 117.0, .bottom = 117.0}, + .dark = {.face = 129.0, .top = 129.0, .side = 129.0, .bottom = 129.0}, + .onBits = TTK_STATE_PRESSED, .offBits = 0 + }, + { + .light = {.face = 182.0, .top = 182.0, .side = 182.0, .bottom = 182.0}, + .dark = {.face = 105.0, .top = 105.0, .side = 105.0, .bottom = 105.0}, + .onBits = TTK_STATE_ACTIVE, .offBits = TTK_STATE_SELECTED + }, + { + .light = {.face = 145.0, .top = 145.0, .side = 145.0, .bottom = 145.0}, + .dark = {.face = 166.0, .top = 166.0, .side = 166.0, .bottom = 166.0}, + .onBits = TTK_STATE_SELECTED, .offBits = 0 + }, + { + .light = {.face = 256.0, .top = 256.0, .side = 256.0, .bottom = 256.0}, + .dark = {.face = 256.0, .top = 256.0, .side = 256.0, .bottom = 256.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign incdecDesign = { + .radius = 5.0, + .palettes = { + { + .light = {.face = 246.0, .top = 236.0, .side = 227.0, .bottom = 213.0}, + .dark = {.face = 80.0, .top = 90.0, .side = 80.0, .bottom = 49.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 198.0, .side = 192.0, .bottom = 173.0}, + .dark = {.face = 118.0, .top = 132.0, .side = 118.0, .bottom = 48.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign bevelDesign = { + .radius = 4.0, + .palettes = { + { + .light = {.face = 242.0, .top = 213.0, .side = 210.0, .bottom = 200.0}, + .dark = {.face = 94.0, .top = 98.0, .side = 94.0, .bottom = 58.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 205.0, .top = 215.0, .side = 211.0, .bottom = 173.0}, + .dark = {.face = 140.0, .top = 150.0, .side = 140.0, .bottom = 42.0}, + .onBits = TTK_STATE_PRESSED, .offBits = 0 + }, + { + .light = {.face = 228.0, .top = 215.0, .side = 211.0, .bottom = 173.0}, + .dark = {.face = 163.0, .top = 150.0, .side = 140.0, .bottom = 42.0}, + .onBits = TTK_STATE_SELECTED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 198.0, .side = 192.0, .bottom = 173.0}, + .dark = {.face = 118.0, .top = 132.0, .side = 118.0, .bottom = 48.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign tabDesign = { + .radius = 4.0, + .palettes = { + + /* + * Apple does not have such a thing as a disabled tab. If it is + * disabled, it should be removed. But we provide one based on the + * disabled button. + */ + + { + .light = {.face = 229.0, .top = 213.0, .side = 242.0, .bottom = 200.0}, + .dark = {.face = 163.0, .top = 90.0, .side = 80.0, .bottom = 49.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 229.0, .top = 205.0, .side = 211.0, .bottom = 183.0}, + .dark = {.face = 163.0, .top = 165.0, .side = 163.0, .bottom = 42.0}, + .onBits = TTK_STATE_SELECTED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 215.0, .side = 211.0, .bottom = 183.0}, + .dark = {.face = 108.0, .top = 129.0, .side = 108.0, .bottom = 47.0}, + .onBits = 0, .offBits = 0 + }, + } +}; + +static const ButtonDesign entryDesign = { + .radius = 0.0, + .palettes = { + { + .light = {.face = 256.0, .top = 198.0, .side = 198.0, .bottom = 198.0}, + .dark = {.face = 256.0, .top = 66.0, .side = 66.0, .bottom = 84.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign searchDesign = { + .radius = 3.5, + .palettes = { + { + .light = {.face = 256.0, .top = 198.0, .side = 198.0, .bottom = 198.0}, + .dark = {.face = 256.0, .top = 66.0, .side = 66.0, .bottom = 84.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign comboDesign = { + .radius = 4.0, + .palettes = { + { + .light = {.face = 256.0, .top = 190.0, .side = 190.0, .bottom = 190.0}, + .dark = {.face = 256.0, .top = 66.0, .side = 66.0, .bottom = 90.0}, + .onBits = 0, .offBits = 0 + } + } +}; + +static const ButtonDesign sliderDesign = { + .radius = 8.0, + .palettes = { + { + .light = {.face = 242.0, .top = 189.0, .side = 198.0, .bottom = 199.0}, + .dark = {.face = 80.0, .top = 84.0, .side = 88.0, .bottom = 60.0}, + .onBits = TTK_STATE_DISABLED, .offBits = 0 + }, + { + .light = {.face = 255.0, .top = 165.0, .side = 184.0, .bottom = 184.0}, + .dark = {.face = 205.0, .top = 205.0, .side = 205.0, .bottom = 198.0}, + .onBits = 0, .offBits = 0 + } + } +}; + + +/* + * Table mapping Tk states to Appearance manager ThemeStates + */ + +static const Ttk_StateTable ThemeStateTable[] = { + {kThemeStateActive, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, + {kThemeStateUnavailable, TTK_STATE_DISABLED, 0}, + {kThemeStatePressed, TTK_STATE_PRESSED, 0}, + {kThemeStateInactive, TTK_STATE_BACKGROUND, 0}, + {kThemeStateUnavailableInactive, TTK_STATE_DISABLED | TTK_STATE_BACKGROUND, 0}, + {kThemeStateActive, 0, 0} + + /* Others: + * The kThemeStatePressedUp and kThemeStatePressedDown bits indicate + * which of the two segments of an IncDec button is being pressed. + * We don't use these. kThemeStateRollover roughly corresponds to + * TTK_STATE_ACTIVE, but does not do what we want with the help button. + * + * {kThemeStatePressedUp, 0, 0}, + * {kThemeStatePressedDown, 0, 0} + * {kThemeStateRollover, TTK_STATE_ACTIVE, 0}, + */ +}; + +/* + * Translation between Ttk and HIToolbox. + */ + +static const Ttk_StateTable ButtonValueTable[] = { + {kThemeButtonOff, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, + {kThemeButtonMixed, TTK_STATE_ALTERNATE, 0}, + {kThemeButtonOn, TTK_STATE_SELECTED, 0}, + {kThemeButtonOff, 0, 0} +}; + +static const Ttk_StateTable ButtonAdornmentTable[] = { + {kThemeAdornmentNone, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, + {kThemeAdornmentDefault | kThemeAdornmentFocus, + TTK_STATE_ALTERNATE | TTK_STATE_FOCUS, 0}, + {kThemeAdornmentFocus, TTK_STATE_FOCUS, 0}, + {kThemeAdornmentDefault, TTK_STATE_ALTERNATE, 0}, + {kThemeAdornmentNone, 0, 0} +}; + +/* + * Our enums for button styles not known to HIToolbox. + */ + +#define TkGradientButton 0x8001 +#define TkRoundedRectButton 0x8002 +#define TkRecessedButton 0x8003 +#define TkInlineButton 0x8004 +/* + * The struct passed as clientData when drawing Ttk buttons. + */ + +typedef struct { + ThemeButtonKind kind; + ThemeMetric heightMetric; + ThemeMetric widthMetric; +} ThemeButtonParams; + +static ThemeButtonParams + PushButtonParams = {kThemePushButton, kThemeMetricPushButtonHeight, + NoThemeMetric}, + CheckBoxParams = {kThemeCheckBox, kThemeMetricCheckBoxHeight, + NoThemeMetric}, + RadioButtonParams = {kThemeRadioButton, kThemeMetricRadioButtonHeight, + NoThemeMetric}, + BevelButtonParams = {kThemeRoundedBevelButton, NoThemeMetric, NoThemeMetric}, + PopupButtonParams = {kThemePopupButton, kThemeMetricPopupButtonHeight, + NoThemeMetric}, + DisclosureParams = {kThemeDisclosureButton, + kThemeMetricDisclosureTriangleHeight, + kThemeMetricDisclosureTriangleWidth}, + DisclosureButtonParams = {kThemeArrowButton, + kThemeMetricSmallDisclosureButtonHeight, + kThemeMetricSmallDisclosureButtonWidth}, + HelpButtonParams = {kThemeRoundButtonHelp, kThemeMetricRoundButtonSize, + kThemeMetricRoundButtonSize}, + ListHeaderParams = {kThemeListHeaderButton, kThemeMetricListHeaderHeight, + NoThemeMetric}, + GradientButtonParams = {TkGradientButton, NoThemeMetric, NoThemeMetric}, + RoundedRectButtonParams = {TkRoundedRectButton, kThemeMetricPushButtonHeight, + NoThemeMetric}, + RecessedButtonParams = {TkRecessedButton, kThemeMetricPushButtonHeight, + NoThemeMetric}, + InlineButtonParams = {TkInlineButton, kThemeMetricPushButtonHeight, + NoThemeMetric}; + + /* + * Others: kThemeDisclosureRight, kThemeDisclosureDown, + * kThemeDisclosureLeft + */ + +/* + * The struct passed as clientData when drawing Ttk Entry widgets. + */ + +typedef struct { + HIThemeFrameKind kind; + ThemeMetric heightMetric; + ThemeMetric widthMetric; +} ThemeFrameParams; + +static ThemeFrameParams + EntryFieldParams = {kHIThemeFrameTextFieldSquare, NoThemeMetric, NoThemeMetric}, + SearchboxFieldParams = {kHIThemeFrameTextFieldRound, NoThemeMetric, NoThemeMetric}; + +/* + * If we try to draw a rounded rectangle with too large of a radius, the Core + * Graphics library will sometimes raise a fatal exception. This macro + * protects against this by returning if the width or height is less than + * twice the radius. Presumably this only happens when a widget has not yet + * been configured and has size 1x1, so there is nothing to draw anyway. + */ + +#define CHECK_RADIUS(radius, bounds) \ + if ((radius) > (bounds).size.width / 2 || (radius) > (bounds).size.height / 2) { \ + return; \ + } + +/* + * The spinbox widget needs to draw the two arrows in different colors when + * one half or the other is being pressed, but the menubutton always draws + * them in the same color. This constant is used to distinguish those two + * situations. + */ + +#define BOTH_ARROWS 1 << 30 + +/* + * Local Variables: + * mode: objc + * c-basic-offset: 4 + * fill-column: 79 + * coding: utf-8 + * End: + */ diff --git a/tests/ttk/combobox.test b/tests/ttk/combobox.test index a972164..d5e031f 100644 --- a/tests/ttk/combobox.test +++ b/tests/ttk/combobox.test @@ -70,16 +70,24 @@ test combobox-2.end "Cleanup" -body { destroy .cb } test combobox-3 "Read postoffset value dynamically from current style" -body { ttk::combobox .cb -values [list a b c] -style "DerivedStyle.TCombobox" pack .cb -expand true -fill both + update idletasks ttk::style configure DerivedStyle.TCombobox -postoffset [list 25 0 0 0] + if {[tk windowingsystem] == "aqua"} { + after 500 { + pressbutton [expr {[winfo rootx .cb] + 25}] [expr {[winfo rooty .cb] + 25}] + } + } ttk::combobox::Post .cb expr {[winfo rootx .cb.popdown] - [winfo rootx .cb]} } -result 25 -cleanup { destroy .cb } -test combobox-1890211 "ComboboxSelected event after listbox unposted" -body { +test combobox-1890211 "ComboboxSelected event after listbox unposted" \ + -constraints {notAqua} -body { # whitebox test... pack [ttk::combobox .cb -values [list a b c]] + update idletasks set result [list] bind .cb <<ComboboxSelected>> { lappend result Event [winfo ismapped .cb.popdown] [.cb get] |