Recent

Author Topic: ThemeServices.DrawText in macOS and Linux/GTK  (Read 379 times)

regs

  • Jr. Member
  • **
  • Posts: 61
ThemeServices.DrawText in macOS and Linux/GTK
« on: February 08, 2025, 11:25:01 pm »
I was trying to make a tag cloud with fancy list or tree item backgrounds from system themes. And while it's working in Windows/Wine and Linux qt5 and qt6, in GTK2 font color is often wrong and in GTK3 and Cocoa font color is same as background.

Windows 11, Windows 7, Wine 10
https://i.imgur.com/83wZGyY.png
https://i.imgur.com/gBrRXFl.png
https://i.imgur.com/V167q0H.png

Linux qt5 and qt6
https://i.imgur.com/JFBUmbj.png
https://i.imgur.com/y3R06eJ.png

Linux GTK2 and GTK3
https://i.imgur.com/9ynm3Th.png
https://i.imgur.com/szSruss.png

macOS Cocoa
https://i.imgur.com/hl6Rwp7.png

I was looking in treeview.inc. And it's pretty much what it's doing:
Code: Pascal  [Select][+][-]
  1. procedure DrawNodeText;
  2. ...
  3. Details := ThemeServices.GetElementDetails(ttItemSelected)
  4. ...
  5. if (tvoThemedDraw in Options) then
  6.     begin
  7.       if not (Enabled and Node.Enabled) then
  8.         Details.State := 4; // TmSchema.TREIS_DISABLED = 4
  9.       ThemeServices.DrawText(Canvas, Details, AText, NdRect, DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_NOPREFIX, 0);
  10.     end
  11.  

Am I missing something?

Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, Themes, LCLIntf, LCLType;
  9.  
  10. type
  11.  
  12.   { TForm1 }
  13.  
  14.   TForm1 = class(TForm)
  15.     Button1: TButton;
  16.     Memo1: TMemo;
  17.     PaintBox1: TPaintBox;
  18.     procedure Button1Click(Sender: TObject);
  19.     procedure PaintBox1Paint(Sender: TObject);
  20.   private
  21.     procedure DrawTagCloud(ACanvas: TCanvas; ATagList, ATagSeparator: String);
  22.   end;
  23.  
  24. var
  25.   Form1: TForm1;
  26.  
  27. implementation
  28.  
  29. {$R *.lfm}
  30.  
  31. { TForm1 }
  32.  
  33. procedure TForm1.Button1Click(Sender: TObject);
  34. begin
  35.   PaintBox1.Invalidate;
  36. end;
  37.  
  38. procedure TForm1.PaintBox1Paint(Sender: TObject);
  39. begin
  40.   DrawTagCloud(PaintBox1.Canvas, Memo1.Text, ' ');
  41. end;
  42.  
  43. procedure TForm1.DrawTagCloud(ACanvas: TCanvas; ATagList, ATagSeparator: String);
  44. const
  45.   PADDING_HORIZONTAL = 4;
  46.   PADDING_VERTICAL = 2;
  47.   SPACING_HORIZONTAL = 6;
  48.   SPACING_VERTICAL = 4;
  49. var
  50.   LThemeDetails: TThemedElementDetails;
  51.   iTextWidth, iTextHeight: integer;
  52.   sWord: string;
  53.   arWordArray: array of String;
  54.   rcBubbleRect: TRect;
  55.   ptPosition, ptPadding, ptSpacing: TPoint;
  56. begin
  57.  
  58.   if (Trim(ATagList) = '') then
  59.     Exit;
  60.  
  61.   arWordArray := Trim(ATagList).Split([ATagSeparator], TStringSplitOptions.ExcludeEmpty);
  62.  
  63.   ptPadding := Point(Scale96ToForm(PADDING_HORIZONTAL), Scale96ToForm(PADDING_VERTICAL));
  64.   ptSpacing := Point(Scale96ToForm(SPACING_HORIZONTAL), Scale96ToForm(SPACING_VERTICAL));
  65.  
  66.   ptPosition.X := 0;
  67.   ptPosition.Y := 0;
  68.   iTextHeight := ACanvas.TextHeight('Ay') + ptPadding.Y*2;
  69.  
  70.   LThemeDetails := ThemeServices.GetElementDetails(TThemedTreeview.ttItemSelected);
  71.   ACanvas.Pen.Style := psClear;
  72.   ACanvas.Brush.Style := bsClear;
  73.   //ACanvas.Font.Color := clHighlightText;
  74.  
  75.   for sWord in arWordArray do
  76.   begin
  77.  
  78.     iTextWidth := ACanvas.TextWidth(sWord) + ptPadding.X*2;
  79.  
  80.     if (ptPosition.X + iTextWidth) > PaintBox1.ClientWidth then
  81.     begin
  82.       ptPosition.Y := ptPosition.Y + iTextHeight + ptSpacing.Y;
  83.       ptPosition.X := 0;
  84.     end;
  85.  
  86.     rcBubbleRect := Rect(ptPosition.X, ptPosition.Y, ptPosition.X + iTextWidth, ptPosition.Y + iTextHeight);
  87.  
  88.     ThemeServices.DrawElement(ACanvas.Handle, LThemeDetails, rcBubbleRect);
  89.     ThemeServices.DrawText(ACanvas.Handle, LThemeDetails, sWord, rcBubbleRect, DT_CENTER or DT_VCENTER or DT_SINGLELINE, 0);
  90.     //LCLIntf.DrawText(ACanvas.Handle, PChar(sWord), Length(sWord), rcBubbleRect, DT_CENTER or DT_VCENTER or DT_SINGLELINE);
  91.  
  92.     ptPosition.X := ptPosition.X + iTextWidth + ptSpacing.X;
  93.  
  94.   end;
  95.  
  96. end;
  97.  
  98. end.
  99.  

All trunk, except 3.8 for macOS

regs

  • Jr. Member
  • **
  • Posts: 61
Re: ThemeServices.DrawText in macOS and Linux/GTK
« Reply #1 on: February 17, 2025, 02:44:29 am »
So, apparently there are several overloaded versions of TThemeServices.DrawText. Some that accept TCanvas and another that accepts HDC handle. TCocoaThemeServices only accept TCanvas one.

LCLGTK3 doesn't have ThemeServices and only TCanvas version of general ThemeServices is working with it, glitching though.
« Last Edit: February 17, 2025, 06:21:43 pm by regs »

regs

  • Jr. Member
  • **
  • Posts: 61
Re: ThemeServices.DrawText in macOS and Linux/GTK
« Reply #2 on: February 17, 2025, 02:59:00 am »
Full code

Code: Pascal  [Select][+][-]
  1. unit TagCloud;
  2.  
  3. {$mode ObjFPC}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, Graphics, Forms, Themes, Math,
  9.   {$IfDef LCLGtk2}
  10.   Gtk2Def,
  11.   {$EndIf}
  12.   {$If Defined(LCLQt5) or Defined(LCLQt6)}
  13.   qtobjects,
  14.   {$EndIf}
  15.   LazLogger,
  16.   LCLIntf, LCLType;
  17.  
  18.   type
  19.     TTagCloudStyle = record
  20.       Padding: TPoint;
  21.       Spacing: TPoint;
  22.       Margin: TPoint;
  23.       BorderWidth: Word;
  24.       BorderRadius: Word;
  25.       BorderColor: TColor;
  26.       BackgroundColor: TColor;
  27.       FontColor: TColor;
  28.       Themed: Boolean;
  29.       Scaled: Boolean;
  30.     end;
  31.  
  32.   function GetDefaultCloudStyle: TTagCloudStyle;
  33.   procedure ScaleTagCloudDesignToForm(var ACloudStyle: TTagCloudStyle; const AForm: TCustomForm);
  34.   procedure ScaleTagCloud(var ACloudStyle: TTagCloudStyle);
  35.   procedure DrawTagCloud(ACanvas: TCanvas; ATagList, ATagSeparator: String; ANoBackground: Boolean = False;
  36.     ASingleLine: Boolean = False);
  37.   procedure DrawTagCloud(ACanvas: TCanvas; ATagList, ATagSeparator: String; ANoBackground: Boolean;
  38.     ASingleLine: Boolean; ACloudStyle: TTagCloudStyle);
  39.  
  40. implementation
  41.  
  42. function GetDefaultCloudStyle: TTagCloudStyle;
  43. const
  44.   PADDING_HORIZONTAL = 4;
  45.   PADDING_VERTICAL = 2;
  46.   SPACING_HORIZONTAL = 6;
  47.   SPACING_VERTICAL = 4;
  48.   OUTER_MARGIN_HORIZONTAL = 2;
  49.   OUTER_MARGIN_VERTICAL = 2;
  50.   BORDER_WIDTH = 0;
  51.   BORDER_RADIUS = 4;
  52. var
  53.   LCloudStyle: TTagCloudStyle;
  54. begin
  55.  
  56.   with LCloudStyle do
  57.   begin
  58.     Padding := Point(PADDING_HORIZONTAL, PADDING_VERTICAL);
  59.     Spacing := Point(SPACING_HORIZONTAL, SPACING_VERTICAL);
  60.     Margin := Point(OUTER_MARGIN_HORIZONTAL, OUTER_MARGIN_VERTICAL);
  61.     BorderRadius := BORDER_RADIUS;
  62.     BorderWidth := BORDER_WIDTH;
  63.     BorderColor := clNone;
  64.     BackgroundColor := clHighlight;
  65.     FontColor := clHighlightText;
  66.     Themed := True;
  67.     Scaled := True;
  68.   end;
  69.  
  70.   Result := LCloudStyle;
  71.  
  72. end;
  73.  
  74. procedure ScaleTagCloudDesignToForm(var ACloudStyle: TTagCloudStyle; const AForm: TCustomForm);
  75. begin
  76.  
  77.   if not Assigned(AForm) then
  78.     Exit;
  79.  
  80.   with ACloudStyle do
  81.   begin
  82.     Padding := Point(AForm.ScaleDesignToForm(Padding.X), AForm.ScaleDesignToForm(Padding.Y));
  83.     Spacing := Point(AForm.ScaleDesignToForm(Spacing.X), AForm.ScaleDesignToForm(Spacing.Y));
  84.     Margin := Point(AForm.ScaleDesignToForm(Margin.X), AForm.ScaleDesignToForm(Margin.Y));
  85.     BorderWidth := AForm.ScaleDesignToForm(BorderWidth);
  86.     BorderRadius := AForm.ScaleDesignToForm(BorderRadius);
  87.     Scaled := False;
  88.   end;
  89.  
  90. end;
  91.  
  92. procedure ScaleTagCloud(var ACloudStyle: TTagCloudStyle);
  93. begin
  94.  
  95.   with ACloudStyle do
  96.   begin
  97.     Padding := Point(ScaleX(Padding.X, 96), ScaleY(Padding.Y, 96));
  98.     Spacing := Point(ScaleX(Spacing.X, 96), ScaleY(Spacing.Y, 96));
  99.     Margin := Point(ScaleX(Margin.X, 96), ScaleY(Margin.Y, 96));
  100.     BorderWidth := ScaleX(BorderWidth, 96);
  101.     BorderRadius := ScaleX(BorderRadius, 96);
  102.     Scaled := False;
  103.   end;
  104.  
  105. end;
  106.  
  107. procedure DrawTagCloud(ACanvas: TCanvas; ATagList, ATagSeparator: String; ANoBackground: Boolean = False;
  108.   ASingleLine: Boolean = False);
  109. begin
  110.  
  111.   DrawTagCloud(ACanvas, ATagList, ATagSeparator, ANoBackground, ASingleLine, GetDefaultCloudStyle);
  112.  
  113. end;
  114.  
  115. // ANoBackground: Don't paint background. In some cases, like it's in selected row.
  116. // ASingleLine: Do not wrap. Useful to fill cells in VirtualStringTree. Height will be measured from ACanvas.ClipRect.
  117. // Debug: LCLGTK3 reports incorrect ClipRect?
  118. // Review: Bounds check. Final X and Y shouldn't be more than half of rect width and hight.
  119. // Review: Font is not well vertically aligned in multiline.
  120. procedure DrawTagCloud(ACanvas: TCanvas; ATagList, ATagSeparator: String; ANoBackground: Boolean;
  121.   ASingleLine: Boolean; ACloudStyle: TTagCloudStyle);
  122. const
  123.   TEXT_STYLE = (DT_CENTER or DT_VCENTER or DT_SINGLELINE or DT_END_ELLIPSIS or DT_MODIFYSTRING);
  124. var
  125.   LThemeDetails: TThemedElementDetails;
  126.   iTextWidth, iTextHeight: integer;
  127.   sTag: string;
  128.   arTagArray: array of String;
  129.   rcBubbleRect, rcBubbleTextRect: TRect;
  130.   ptPosition: TPoint;
  131.  
  132.   function ThemeCapeable(DC: HDC): Boolean;
  133.   {$IfDef LCLGtk2}
  134.   var
  135.     DevCtx: TGtkDeviceContext absolute DC;
  136.   {$EndIf}
  137.   {$If Defined(LCLQt5) or Defined(LCLQt6)}
  138.   var
  139.     Context: TQtDeviceContext;
  140.   {$EndIf}
  141.   begin
  142.  
  143.     Result := False;
  144.     if not ThemeServices.ThemesEnabled then
  145.       Exit;
  146.     if not ThemeServices.ThemesAvailable then
  147.       Exit;
  148.  
  149.     {$IfDef LCLWin32}
  150.     if (Win32MajorVersion >= 6) then
  151.       Exit(True);
  152.     {$EndIf}
  153.  
  154.     {$IfDef LCLGtk2}
  155.     // In some occasions Widget could not be found, like VirtualStringTree's cell.
  156.     if DevCtx.Widget <> nil then
  157.       Exit(True);
  158.     {$EndIf}
  159.  
  160.     {$IfDef LCLGtk3}
  161.     // No theme service for GTK3 as of moment
  162.     Exit;
  163.     {$EndIf}
  164.  
  165.     {$If Defined(LCLQt5) or Defined(LCLQt6)}
  166.     // In some occasions Context could not be found, like VirtualStringTree's cell.
  167.     Context := TQtDeviceContext(DC);
  168.     if Context.Parent <> nil then
  169.       Exit(True);
  170.     {$EndIf}
  171.  
  172.   end;
  173.  
  174. begin
  175.  
  176.   if (Trim(ATagList) = '') then
  177.     Exit;
  178.  
  179.   // Splitting text into array of tags
  180.   arTagArray := Trim(ATagList).Split([ATagSeparator], TStringSplitOptions.ExcludeEmpty);
  181.  
  182.   // Scaling all dimensions to screen
  183.   if ACloudStyle.Scaled then
  184.     ScaleTagCloud(ACloudStyle);
  185.  
  186.   // Styling
  187.   if not ThemeCapeable (ACanvas.Handle) then
  188.     ACloudStyle.Themed := False;
  189.  
  190.   if ACloudStyle.Themed then
  191.   begin
  192.     LThemeDetails := ThemeServices.GetElementDetails(TThemedTreeview.ttItemSelected);
  193.   end
  194.   else
  195.   begin
  196.     ACanvas.Pen.Style := psSolid;
  197.     ACanvas.Pen.Width := ACloudStyle.BorderWidth;
  198.     ACanvas.Pen.Color := ACloudStyle.BorderColor;
  199.     ACanvas.Brush.Color := ACloudStyle.BackgroundColor;
  200.     ACanvas.Brush.Style := bsClear;
  201.     if not ANoBackground then
  202.       ACanvas.Font.Color := ACloudStyle.FontColor;
  203.   end;
  204.  
  205.   if ACloudStyle.Themed or (ACloudStyle.BorderWidth = 0) then
  206.   begin
  207.     ACloudStyle.BorderWidth := 0;
  208.     ACanvas.Pen.Style := psClear;
  209.   end;
  210.  
  211.   // Dimensions
  212.  
  213.   // If border width is 1, then there  no external border
  214.   if ACloudStyle.BorderWidth = 1 then
  215.     ACloudStyle.BorderWidth := 0;
  216.  
  217.   // If odd external border is 1 px smaller than internal. Making even for dimensions.
  218.   if Odd(ACloudStyle.BorderWidth) then
  219.     Inc(ACloudStyle.BorderWidth, 1);
  220.  
  221.   // Applying half of two borders, as half is rendered outside
  222.   ptPosition := Point(ACanvas.ClipRect.Left + ACloudStyle.BorderWidth, ACanvas.ClipRect.Top + ACloudStyle.BorderWidth);
  223.  
  224.   // Initial position
  225.   Inc(ptPosition.X, ACloudStyle.Margin.X);
  226.   Inc(ptPosition.Y, ACloudStyle.Margin.Y);
  227.  
  228.   // Calculating height
  229.   if ASingleLine then
  230.     iTextHeight := ACanvas.ClipRect.Height - ACloudStyle.Margin.Y*2 + ACloudStyle.BorderWidth
  231.   else
  232.     iTextHeight := ACanvas.TextHeight('Ay') + ACloudStyle.Padding.Y*2 + ACloudStyle.BorderWidth;
  233.  
  234.   // Iterating tags
  235.   for sTag in arTagArray do
  236.   begin
  237.  
  238.     // Calculating width
  239.     iTextWidth := ACanvas.TextWidth(Trim(sTag)) + ACloudStyle.Padding.X*2  + ACloudStyle.BorderWidth;
  240.  
  241.     // New line, if overflows
  242.     if ((ptPosition.X + iTextWidth + ACloudStyle.Margin.X) > ACanvas.ClipRect.Right) and not(ASingleLine) then
  243.     begin
  244.       ptPosition.Y := ptPosition.Y + iTextHeight + ACloudStyle.Spacing.Y + ACloudStyle.BorderWidth;
  245.       ptPosition.X := ACanvas.ClipRect.Left + ACloudStyle.Margin.X + ACloudStyle.BorderWidth;
  246.     end;
  247.  
  248.     // Edging by right, if we are not going for new line
  249.     if (ptPosition.X + iTextWidth + ACloudStyle.Margin.X) > ACanvas.ClipRect.Right then
  250.       iTextWidth := ACanvas.ClipRect.Right - ptPosition.X - ACloudStyle.Margin.X;
  251.  
  252.     //Calculating main rectangle
  253.     rcBubbleRect := Rect(ptPosition.X, ptPosition.Y, ptPosition.X + iTextWidth, ptPosition.Y + iTextHeight);
  254.  
  255.     // Calculating text rectangle
  256.     rcBubbleTextRect := rcBubbleRect;
  257.     Inc(rcBubbleTextRect.Left, ACloudStyle.Padding.X + Math.Ceil(ACloudStyle.BorderWidth/2));
  258.     Dec(rcBubbleTextRect.Right, ACloudStyle.Padding.X + Math.Ceil(ACloudStyle.BorderWidth/2));
  259.  
  260.     // Drawing
  261.     if ACloudStyle.Themed then
  262.     begin
  263.       ACanvas.Brush.Style := bsSolid;
  264.       if (not ANoBackground) then
  265.         ThemeServices.DrawElement(ACanvas.Handle, LThemeDetails, rcBubbleRect);
  266.       ACanvas.Brush.Style := bsClear;
  267.       ThemeServices.DrawText(ACanvas, LThemeDetails, Trim(sTag), rcBubbleTextRect, TEXT_STYLE, 0);
  268.     end
  269.     else
  270.     begin
  271.       if ACloudStyle.BackgroundColor <> clNone then
  272.         ACanvas.Brush.Style := bsSolid;
  273.       if not ANoBackground then
  274.         ACanvas.RoundRect(rcBubbleRect, ACloudStyle.BorderRadius, ACloudStyle.BorderRadius);
  275.       ACanvas.Brush.Style := bsClear;
  276.       LCLIntf.DrawText(ACanvas.Handle, PChar(Trim(sTag)), Length(Trim(sTag)), rcBubbleTextRect, TEXT_STYLE);
  277.     end;
  278.  
  279.     // Calculating position for next tag
  280.     ptPosition.X := ptPosition.X + iTextWidth + ACloudStyle.Spacing.X + ACloudStyle.BorderWidth;
  281.  
  282.     // Abort, if we reached the end in single line mode
  283.     if (ptPosition.X >= ACanvas.ClipRect.Right) and ASingleLine then
  284.       Exit;
  285.  
  286.   end;
  287.  
  288. end;
  289.  
  290. end.
  291.  

and example


 

TinyPortal © 2005-2018