TurboPack / SynEdit

SynEdit is a syntax highlighting edit control, not based on the Windows common controls.
221 stars 73 forks source link

Font problem #186

Closed BorisU7 closed 2 years ago

BorisU7 commented 2 years ago

When I try to change the font in SynEdit, in some cases the font is rejected by the IsFontMonospacedAndValid procedure despite being a valid monospaced scalable font. I had this issue with a Cascadia Code font (Cascadia Code) when the font style is not one of Regular, Italic, Bold, Bold Italic. I can preview these rejected font variations in TMemo control and they look fine. Far Manager also has no problems.

The worst thing is that after the attempt to set such a font SynEdit switches to some unrelated proportional font (may be the Parent font?). image

pyscripter commented 2 years ago

Segoe UI is not monospaced. SynEdit reverts to Consolas if the font is not accepted. Which Windows version are you using?

pyscripter commented 2 years ago

If you want say Cascadia Code Bold as your base font, set the font as Cascadia Code and add Bold to the font style.

BorisU7 commented 2 years ago

Windows 10 with latest updates, Scale is 125% if it is important. Segoe UI is used for the program interface, not the editor. 'Change' button in this dialog execute a TFontDialog that has a Font property. I do not modify it. Then it is applied to the preview TMemo and you can see that the font is perfectly valid. Then the same Font is applied to the SynEdit component and this operation fails.

If I choose in the TFontDialog 'Cascadia Code PL' font with Regular or Italic style - everything goes smooth. Problems arise only for Light, SemiBold etc. styles of the same font.

BorisU7 commented 2 years ago

Just made a test with another font that has many styles: 'Source Code Pro' Behavior is the same. Regular, Italic etc. styles work, others do not.

MShark67 commented 2 years ago

I don't seem to have this issue. Does it matter how the font is changed? I separately set SynEdit1.Font.Name,size, style, etc to the values I get back from my font dialog's font. Maybe it matters if you do that vs setting SynEdit1.Font := MyNewFont? I hope that makes sense and I'm not just breaking in on a topic that I'm not really that knowledgeable about! I'll experiment and see if I can cause it.

BorisU7 commented 2 years ago

I do it the simplest way.

Editor.Font.Assign(GlobalOptions.EditorFont); Editor.Gutter.Font.Assign(GlobalOptions.EditorFont);

GlobalOptions.EditorFont is what I get from TFontDialog.Font

MShark67 commented 2 years ago

I can't seem to get it to fail on mine no matter how I assign it, direct equals, .assign or my separate font properties method. I can set it to all of those various fonts listed along with the semilight, black, medium, etc and they do seem to work and to display properly. I wonder if it's being rejected because the font name isn't found in your Screen.Fonts list, or if the font is reporting that it's not monospaced, or if the "can it be used by direct write" is failing. I'm guessing it's the first, but I don't know why the font list might be messed up? Can you breakpoint the IsFontMonospacedAndValid function in SynEditMiscProcs to find out what's failing?

MShark67 commented 2 years ago

I spoke too soon! I definitely see some issues now, though I haven't been able to get IsFontMonospacedAndValid itself to fail. I think I'm barking up the wrong tree. Will report back.

MShark67 commented 2 years ago

Fascinating! It looks to me like IsFontMonospacedAndValid works fine, but then when the TFont gets converted to a DWFont it gets changed. I found a stack overflow answer that talks about this (I think) DWFont question

BorisU7 commented 2 years ago

Strange, now I can't reproduce the exception in IsFontMonospacedAndValid :( May be the problem is that Delphi TFont doesn't work correctly with Weight returned from WinAPI call?

BorisU7 commented 2 years ago

Debugged it through VCL code. WinAPI returns a LOGFONT structure. In procedure TFontDialog.UpdateFromLogFont delphi sets up the corresponding TFont object. And practically ignores the lfWeight parameter. In the case of Cascadia Code Light it is equal to 300. Default value for regular font is 400.

MShark67 commented 2 years ago

I'm fairly sure it all comes down to TSynTextFormat.Create and how it converts TFont to be used in DWrite. The TFont.Name is family plus the extra style/weight info that doesn't fit the old TFontStyle enum. The DWrite.CreateTextFormat takes a font family and if you pass it the TFont.Name unmodified with the extra info, then it does something... strange lol.

pyscripter commented 2 years ago

In

  CheckOSError(TSynDWrite.DWriteFactory.CreateTextFormat(PChar(AFont.Name), nil,
    DWFontWeight, DWFontStyle, DWRITE_FONT_STRETCH_NORMAL,
    -AFont.Height, DefaultLocaleName, FIDW));
  FIDW.SetIncrementalTabStop(TabWidth * FCharWidth);

DWFontWeight should be based on LogFont.lfWeight and not on AFont.Style.
And the first argument should be the FontFamily name which is quite fiddly to get https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritefontfamily-getfamilynames

pyscripter commented 2 years ago

There are two problems:

MShark67 commented 2 years ago

I have code that gets the correct family name and passes it, but neither DWFont.GetWeight or LogFont.lfWeight seem to work. I'm testing with Source code pro light and DWFont.GetWeight just returns DWRITE_FONT_WEIGHT_NORMAL (as does LogFont.lfWeight.) So at least now it works, it just throws away the extra weight info. I'll look more.

pyscripter commented 2 years ago

Can you post the code for getting correct Family Name. I can get the correct lfWeight via EnumFontFamiliesEx!

MShark67 commented 2 years ago

This is what I've been playing with (as you can see it's a bit rough):

  DWFont.GetFontFamily(FontFam);
  FontFam.GetFamilyNames(Names);
  for var i := 0 to Names.GetCount - 1 do
  begin
    Names.GetStringLength(i, l);
    SetLength(S, l);
    Names.GetString(i, PChar(S), Length(S) + 1);
    FamilyName := S;
  end;
MShark67 commented 2 years ago

vars for the above:

  FontFam: IDWriteFontFamily;
  Names: IDWriteLocalizedStrings;
  l: Cardinal;
  s: String;
  FamilyName: String;
pyscripter commented 2 years ago

Your code seams to be using the last name if they are many. Don't you need something like what is described in https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritefontfamily-getfamilynames?

SynDWrite sets the DefaultLocaleName

    {
        hr = pFamilyNames->FindLocaleName(localeName, &index, &exists);
    }
    if (SUCCEEDED(hr) && !exists) // if the above find did not find a match, retry with US English
    {
        hr = pFamilyNames->FindLocaleName(L"en-us", &index, &exists);
    }
// If the specified locale doesn't exist, select the first on the list.
if (!exists)
    index = 0;

UINT32 length = 0;

// Get the string length.
if (SUCCEEDED(hr))
{
    hr = pFamilyNames->GetStringLength(index, &length);
}
MShark67 commented 2 years ago

I'm sure that's true! I was kind of excited just to get something that seemed to give the name I was looking for lol. I can check that out and try to make something less rough if you'd like.

pyscripter commented 2 years ago

What you did was helpful. I will try to combine it with what I am doing regarding the weight. Thanks!

MShark67 commented 2 years ago

Here's an improved version (somewhat):

  DWFont.GetFontFamily(FontFam);
  FontFam.GetFamilyNames(Names);
  if Names.GetCount > 0 then
  begin
    Names.FindLocaleName(DefaultLocaleName, Index, Exists);
    if not Exists then
      Index := 0;
    Names.GetStringLength(Index, NameLength);
    SetLength(FamilyName, NameLength);
    Names.GetString(Index, PChar(FamilyName), Length(FamilyName) + 1);
  end
  else
    raise Exception.Create('Font not found.');
pyscripter commented 2 years ago

Does this work?

  DWFont.GetFontFamily(FontFam);
  FontFam.GetFamilyNames(Names);
  if Names.GetCount > 0 then
  begin
    Names.FindLocaleName(DefaultLocaleName, Index, Exists);
    if not Exists then
    begin
      Names.FindLocaleName('en-us', Index, Exists);
      if not Exists then
        Index := 0;
    end;
    Names.GetStringLength(Index, NameLength);
    SetLength(FamilyName, NameLength);
    Names.GetString(Index, PChar(FamilyName), Length(FamilyName) + 1);
  end
  else
    raise Exception.Create('Family Name not found');
MShark67 commented 2 years ago

Tested with a bunch of fonts. No problems seen. Great job! Thanks!

MShark67 commented 2 years ago

Found a strange issue. Any font where you can choose "Medium" as the style ends up with a FontStyle of fsBold and so ends up with a DWFont.Weight of bold instead of medium which causes the CharWidth to end up too wide for the displayed font. I see this on: Fira Code Medum JetBrains Mono Medium Source Code Pro Medium If I bypass the TFontStyle check in GetCorrectFontWeight it fixes the issue. I'm not sure what the workaround would be (since it seems like a TFontDialog/TFont bug) but perhaps we could get the enumerated font weight first and only check for fsBold if that result was FW_NORMAL? I'm only half awake right now so that may be a terrible idea lol. I'm happy to look into this more or do anything else you'd like to assign to me.

BorisU7 commented 2 years ago

I am not a big specialist in the windows internals but ... As you can see from my screenshot Memo control is perfectly fine with TFont and shows the correct result. So probably one can select this TFont into the DC context and get the complete info for it with correct lfWeight etc.

BorisU7 commented 2 years ago

Got updated code from git. The problem seems fixed for me.

BorisU7 commented 2 years ago

Sorry, but I found one more issue.

Here is the comparison between the same font rendered in TMemo and TSynEdit image

Screenshot is made with scaling at 200% where the difference is clear.

Some further investigation showed that this is not a problem of SynEdit. Current SynEdit code properly shows ExtraLight style thinner than Light. While in TMemo ExtraLight is somehow thicker.

pyscripter commented 2 years ago

@BorisU7 I think SynEdit is indeed now rendering the ExtraLight weight properly. Your Memo is just showing the standard weight. As you said Vcl is ignoring the font weight.

@MShark67 Good catch with Medium. The issue stems from Vcl setting bold to true when you assign a font with weight greater than 400 (normal) needs a bit of thinking. At least on my part.