Thread Tools Display Modes
06/08/24, 02:07 PM   #1
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
Perhaps the silliest question for this week?

Disclaimer: I'm in the weeds, I've looked high, low, and lower. This is an annoyance that has become a thing ...

The Goal:

I'm trying to print info to the chat window in an aligned fashion. I think I am fighting a proportional font issue that prevents me from being able to line things up. At least, that's what I'm telling myself at the moment. Am I wrong?

The code:
Code:
d(string.format("Bottom test 1: Count (%2d): %10s = %-40s!", 1, 22, "Hello"));
d(string.format("Bottom test 2: Count (%2d): %10s = %-40s!", 2, 32, "Hello There"));
d(string.format("Bottom test 3: Count (%2d): %10s = %-40s!", 3, 42, "           ii Well Hello There"));
The output:


Notice that the end of each string (marked by the ! character) is staggered, roughly by the length of the string preceding it. The exclamation points should be lining up?


The problem (as I understand it):

sprintf should be left justifying the three varying length texts (Hello, Hello There, etc.) and the exclamation point at the end should be in alignment with the one above. But, and hopefully this is obvious in the post, the string in the %-40s section isn't behaving as expected.

What is odd to me is that the second %10s strings (22,32,42) seem to be working okay though, granted, they are just numbers so maybe there isn't a lot of variance and this string definition is misbehaving like the 3rd, just not as obviously.

Either way, the question: Is there a way to properly format/align this type of output?

I've mined zo_strformat, lua's various controls, the wiki and source wherever I could find it - and am trying to avoid opening a whole new window layer where I can control the font ("avoid" is code for "I've tried 2 or 3 different examples/approaches, each resulting in a dismal failure and so I'm really hoping the solution isn't that complicated?")

Last edited by Vilkasmanga : 06/08/24 at 02:54 PM.
  Reply With Quote
06/08/24, 03:14 PM   #2
sirinsidiator
 
sirinsidiator's Avatar
AddOn Author - Click to view addons
Join Date: Apr 2014
Posts: 1,589
You are correct about that. Lua's string.format function is very low level and has no knowledge about fonts, typesetting and everything else that's going on in something as high level as the ESO UI. It simply outputs a number of whitespace characters, which works fine as long as you use a monospaced font.

Your best option is to make use of the padding markup provided by the game to align your text.
That will work somewhat fine as long as the user doesn't use an addon to change what font the chat uses.

Here's an example how I solved it in LibChatMessage:
Lua Code:
  1. local out = {}
  2. out[#out + 1] = "/chatmessage <command> [argument]"
  3. out[#out + 1] = "<time>|u129%:0:  :|u[on/off]|u286%:0:       :|uEnables or disables the time prefix"
  4. out[#out + 1] = "<chat>|u125%:0:  :|u[on/off]|u288%:0:       :|uShow time prefix on regular chat"
  5. out[#out + 1] = "<format>|u62%:0: :|u[auto/12h/24h]|u68%:0:  :|uChanges the time format used"
  6. out[#out + 1] = "<tag>|u165%:0:   :|u[off/short/long]|u50%:0::|uControls how a message is tagged"
  7. out[#out + 1] = "<history>|u50%:0::|u[on/off]|u286%:0:       :|uRestore old chat after login"
  8. out[#out + 1] = "<age>|u147%:0:   :|u[seconds]|u200%:0:      :|uThe maximum age of restored chat"
  9. out[#out + 1] = "Example: /chatmessage tag short"
  10. chat:Print(tconcat(out, "\n"))

Other than that, aligning text is an incredibly difficult problem to solve, especially if you want to make it work with different fonts, sizes, resolutions etc.
  Reply With Quote
06/08/24, 04:41 PM   #3
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
sirinsidiator - Thank you!

I am glad I wasn't missing something obvious but also greatly appreciate your suggestion - I'm off to work on that!

-V
  Reply With Quote
06/09/24, 01:47 PM   #4
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
Update: Workaround in place, things look better - good enough for the next update in a week or two. Thanks go to sirinsidiator, et al.
  • Today's lesson: Ya gotta listen to the experts, especially when they drop key hints like NPC's....
    That will work somewhat fine as long as the user doesn't use an addon to change what font the chat uses.[/i] - sirinsidiator
    This is critical in my case since I use PChat and one of the things it does is change the output of d(...) which impacts the |u directives (as in, it removes the padding spaces). We won't dwell on how many hours I spent debugging / cursing code that was otherwise perfectly innocent. The good news is there is a setting inside of that excellent AddOn to adjust for that but I don't want to conflict with other Addons, especially big ones like PChat (or make people have to change their setups just for FTK).

    Update: PChat may not be the problem here. I saw the problem, made a change to Pchat, the problem went away - however, I'm now unable to replicate it so let's take Pchat out of the possible reasons list. The lesson of "check your addons" still applies but PChat isn't a good example. (I've also since updated PChat so my test environ has changed and the problem has gone away so... if I rediscover it, I'll post again).


    So, I'll use a different approach: ugly manual padding (per suggestion)

  • Here is some ugly but functioning code that will line text up...mostly.

    - GetStringWidth(text, font) returns the # of pixels for a given string (text), based on the font provided (ZoChatFont, typically)

    - fixwidth(string, width) takes a supplied string and pads it out to the requested width (string length)

    Code:
    -- Calculate /return the width of a string in pixels based on a supplied font (typically ZoFontChat)
    local function GetStringWidth(text, font)
        local control = WINDOW_MANAGER:CreateControl(nil, GuiRoot, CT_LABEL)
        control:SetFont(font)
        control:SetText(text)
        local width = control:GetTextWidth()
        control:SetHidden(true)
        return width
    end -- GetStringWidth
    
    
    
    
    -- fixwidth(str, width) - Return a padded string of "width" characters wide based on the supplied ("str") 
    -- TODO: Add "font" argument to support this calculation for somethign besides ZoFontChat
    
    local function fixwidth(str, width)
    
       -- Step 0: Choose a spacing character. 
       --        Unfort, it cannot be a blank, as those get special and unhelpful treatment (length=0)
       local spacechar = "-";
    
       -- Step 1: Determine the length of the string that was passed
       local strpix=FarmersToolkit.GetStringWidth(str,"ZoFontChat"); -- Default length of a "character" for this purpose
       local strlen1=string.len(str);
    
       -- Step 2: Determine the width of a _ using the standard font (We'll want to make this assignable later)
       local spacer=FarmersToolkit.GetStringWidth(spacechar, "ZoFontChat");
    
       -- Step 3: Determine how much space is needed for padding
       local targetpadding=math.floor(((width*spacer)-strpix)/spacer);
    
       -- Step 4: Format the string with the appropriate padding
      if ( type(str) == "nil" ) then return "" end
    
         -- Curiously, this works better than a substr approach.  This gets the pixel width to within 1 strpi (pixel count of the spacing char)
         local tempret=str;
         while (FarmersToolkit.GetStringWidth(tempret, "ZoFontChat") < (width*spacer) ) do
            tempret = tempret .. spacechar;
         end
         retval = tempret;
    
       -- Step 5: Check results (dft_debug is just a conditional d(...) clone, safely deleted if not part of FTK
            local checkvar = FarmersToolkit.GetStringWidth(retval,"ZoFontChat");
    
            dft_debug("FW2("..str..", "..width.."): strpix=" .. strpix .. ", strlen1=" .. strlen1 .. ", Spacer char=[" .. spacechar .. "], spacer len=" .. spacer .. ", targetpadding=" .. targetpadding);
            dft_debug("Return string is [" .. retval .. "], retval len=" ..checkvar .." pixels wide versus the target of " .. (width*spacer) .. ", delta = " .. (width*spacer)-checkvar .. " (versus spacer length of " ..spacer)
            dft_debug(" ");
    
       -- Step 6: return results
    
      return retval
    
    end
    -- end fixwidth

This mostly works, enough for me to stop chasing the perfect solution (for now). Things typically line up (within a few pixels, typically < 3) and looks okay in chat which is good enough (for now).

@sirinsidiator - Thanks again for your help. Formatting aligned output remains a painful endeavor but less so with your suggestions.


-V

Last edited by Vilkasmanga : 06/13/24 at 09:33 PM.
  Reply With Quote
06/09/24, 03:15 PM   #5
Baertram
Super Moderator
 
Baertram's Avatar
WoWInterface Super Mod
AddOn Author - Click to view addons
Join Date: Mar 2014
Posts: 5,060
This is critical in my case since I use PChat and one of the things it does is change the output of d(...) which impacts the |u directives (as in, it removes the padding spaces)
Any intel from your debugging what in pChat does that change?

Maybe we can find a way to fix it (if it's not beaking any pChat functionality or invalidating the saved history in the SavedVars).
  Reply With Quote
06/09/24, 04:59 PM   #6
DakJaniels
AddOn Author - Click to view addons
Join Date: Mar 2021
Posts: 37
Did you see how they handle markup here? maybe it could be of some use to you.

https://github.com/esoui/esoui/blob/...hared.lua#L433
  Reply With Quote
06/09/24, 07:30 PM   #7
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
Baertram - I didn't write anything down, just pivoted when I saw the behavior but I will try to retrace my steps and let you know how that goes. As memory serves, there was code similar to the following that wasn't spaced under one configuration, changing a single checkbox in PChat resolved it. I'll try and find that setting. I'm assuming it was a default but.... we shall see.

The code, which includes sirinsidiator's stuff from above, with variations:

Code:
dft( zo_strformat("<time>|u129%:0:  :|u[on/off]|u286%:0:       :|uEnables or disables the time prefix" .. "\n"));
dft( zo_strformat("<chat>|u125%:0:  :|u[on/off]|u288%:0:       :|uShow time prefix on regular chat" .. "\n"));
dft( zo_strformat("Test 1: I am here and |cFF0000apple colored|r and you |u50:0::are here|r"));
dft( zo_strformat("Test 2: I am here and |cFF0000apple colored|r and you |u50:0::are here|r"));
dft( "Test 3: I am here and |cFF0000apple colored|r and you |u50:0::are here|r - via dft");
d  ( "Test 4: I am here and |cFF0000apple colored|r and you |u50:0::are here|r - via d");

     dft("Proof of concept");
     local out = {}
     out[#out + 1] = "/chatmessage <command> [argument]" .. "\n";
     out[#out + 1] = "<time>|u129%:0:  :|u[on/off]|u286%:0:       :|uEnables or disables the time prefix" .. "\n";
     out[#out + 1] = "<chat>|u125%:0:  :|u[on/off]|u288%:0:       :|uShow time prefix on regular chat" .. "\n";
     out[#out + 1] = "<format>|u62%:0: :|u[auto/12h/24h]|u68%:0:  :|uChanges the time format used" .. "\n";
     out[#out + 1] = "<tag>|u165%:0:   :|u[off/short/long]|u50%:0::|uControls how a message is tagged" .. "\n";
     out[#out + 1] = "<history>|u50%:0::|u[on/off]|u286%:0:       :|uRestore old chat after login" .. "\n";
     out[#out + 1] = "<age>|u147%:0:   :|u[seconds]|u200%:0:      :|uThe maximum age of restored chat" .. "\n";
     out[#out + 1] = "Example: /chatmessage tag short" .. "\n";

     d(table.concat(out,"\n"));
and for reference, dft is just a wrapper for d...

Code:
local function dft(line)
    if ( ( FarmersToolkit.FTREP == 1 ) and (type(line) ~= nil) )  then
                d(FarmersToolkit.FTChat .. line);
    end
end  -- function dft
  Reply With Quote
06/09/24, 07:33 PM   #8
Baertram
Super Moderator
 
Baertram's Avatar
WoWInterface Super Mod
AddOn Author - Click to view addons
Join Date: Mar 2014
Posts: 5,060
Hint: type(line) will never be nil, it will be "nil" (returns a string)
-> Learned that myself yesterday
  Reply With Quote
06/09/24, 07:39 PM   #9
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
Originally Posted by DakJaniels View Post
Did you see how they handle markup here? maybe it could be of some use to you.

https://github.com/esoui/esoui/blob/...hared.lua#L433
I did look here but will look again, I think my original approach was still in the mode of sprintf, meaning I was looking at something to "print this string within 50 spaces" aka: sprintf("%50s","blah") but this code, along with sirinsidiator's suggestions, offers a different way: "string 1" "padding component" "string 2", treating the padding as a separate (versus embedded) component. I'll take another run at it.

I also realized a flaw in my approach of using string length on a link, since the link looks like 10-15 characters but is in fact longer. So, the new approach calculates based on the number of characters in what appears (versus everything that is embedded in the link string which, when printed, may only show 10-15 characters). The new code handles that problem and opens up the door for the approaches here and in sirinsidiator's suggestions as well.

Thanks for the suggestion !

-V
  Reply With Quote
06/09/24, 07:39 PM   #10
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
Originally Posted by Baertram View Post
Hint: type(line) will never be nil, it will be "nil" (returns a string)
-> Learned that myself yesterday
Doh! Old code! Good point, will fix before posting a new version, lol.

-V
  Reply With Quote
06/10/24, 05:26 AM   #11
sirinsidiator
 
sirinsidiator's Avatar
AddOn Author - Click to view addons
Join Date: Apr 2014
Posts: 1,589
why check for type(line) ~= "nil" when you can just check for line ~= nil? there is only one value that can be of type nil, so it doesn't make much sense to call a function first to get the type (unless you store the result and check for multiple different types).
  Reply With Quote
06/10/24, 06:24 AM   #12
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
Originally Posted by sirinsidiator View Post
why check for type(line) ~= "nil" when you can just check for line ~= nil? there is only one value that can be of type nil, so it doesn't make much sense to call a function first to get the type (unless you store the result and check for multiple different types).
Honestly, because this was some of the earliest code ("old code", as I said to Baer) that I'd ever written in lua and kept running into nil variable issues -- especially in cases where a previously defined variable would show up as nil, that kept biting me so... this was one of many coping strategies

I started this all less than 6 months ago, having never worked with lua so the learning curve started pretty low. With the help of folks like you, Baertram, Dolgubon and tons of documentation, pointers, suggestions, however, the learning curve has been steep but productive. But yeah, I still have "day one" code that probably needs a re-write. I'm in a kind of liminal space with respect to the addon: not completely lost yet far from being fluent so I'm definitely still in transition. I need to find the balance between "keep learning / trying new things" versus "go back and clean things up". Suggestions like yours show the right / more efficient ways of doing things and I try to fold those into new code (don't always succeed, I'll backslide with type() when the debugging is a slog) but in general, new code today is less cumbersome (though far from efficient or elegant) than last month's or the month before, etc. etc.

It is a journey, to be sure, and I appreciate everyone's help along the way. There is a good chance I'll slow development down for a while. Having just posted the latest update, maybe this is a good time to take a break, go back and clean things up. Heck, I may even go crazy and try once more to break things into separate files - crazy talk, I know

Thanks again for your help.

-V
  Reply With Quote
06/10/24, 07:02 AM   #13
Baertram
Super Moderator
 
Baertram's Avatar
WoWInterface Super Mod
AddOn Author - Click to view addons
Join Date: Mar 2014
Posts: 5,060
You could even just skip the nil check if it's only about chat output for debugging:
Lua Code:
  1. local function dft(line)
  2.     if FarmersToolkit.FTREP == 1  then
  3.                 d(FarmersToolkit.FTChat .. tostring(line));
  4.     end
  5. end  -- function dft

The tostring converts the nil to "nil" so you just see in your debug msg that something was wrong.
Or if line usually contains a space at the beginning (not sure if the space is at the end of your FarmersToolkit.FTChat already)
you could even do this to add the space if line is nil:

Lua Code:
  1. local function dft(line)
  2.     if FarmersToolkit.FTREP == 1  then
  3.                 line = line or " "
  4.                 d(FarmersToolkit.FTChat .. line);
  5.     end
  6. end  -- function dft


You are welcome to join us here for code questions, "best practices" about splitting up into multiple files et such:
https://app.gitter.im/#/room/#esoui_esoui:gitter.im

It's often easier to talk about actual code if you can drag&drop files, post blocks more easily and use links to the esoui sources without having to create forum threads and posts.

Last edited by Baertram : 06/10/24 at 07:07 AM.
  Reply With Quote
06/13/24, 09:45 PM   #14
Vilkasmanga
AddOn Author - Click to view addons
Join Date: Dec 2023
Posts: 17
Originally Posted by Vilkasmanga View Post
Baertram - I didn't write anything down, just pivoted when I saw the behavior but I will try to retrace my steps and let you know how that goes. As memory serves, there was code similar to the following that wasn't spaced under one configuration, changing a single checkbox in PChat resolved it. I'll try and find that setting. I'm assuming it was a default but.... we shall see.

The code, which includes sirinsidiator's stuff from above, with variations:

Code:
dft( zo_strformat("<time>|u129%:0:  :|u[on/off]|u286%:0:       :|uEnables or disables the time prefix" .. "\n"));
dft( zo_strformat("<chat>|u125%:0:  :|u[on/off]|u288%:0:       :|uShow time prefix on regular chat" .. "\n"));
dft( zo_strformat("Test 1: I am here and |cFF0000apple colored|r and you |u50:0::are here|r"));
dft( zo_strformat("Test 2: I am here and |cFF0000apple colored|r and you |u50:0::are here|r"));
dft( "Test 3: I am here and |cFF0000apple colored|r and you |u50:0::are here|r - via dft");
d  ( "Test 4: I am here and |cFF0000apple colored|r and you |u50:0::are here|r - via d");

     dft("Proof of concept");
     local out = {}
     out[#out + 1] = "/chatmessage <command> [argument]" .. "\n";
     out[#out + 1] = "<time>|u129%:0:  :|u[on/off]|u286%:0:       :|uEnables or disables the time prefix" .. "\n";
     out[#out + 1] = "<chat>|u125%:0:  :|u[on/off]|u288%:0:       :|uShow time prefix on regular chat" .. "\n";
     out[#out + 1] = "<format>|u62%:0: :|u[auto/12h/24h]|u68%:0:  :|uChanges the time format used" .. "\n";
     out[#out + 1] = "<tag>|u165%:0:   :|u[off/short/long]|u50%:0::|uControls how a message is tagged" .. "\n";
     out[#out + 1] = "<history>|u50%:0::|u[on/off]|u286%:0:       :|uRestore old chat after login" .. "\n";
     out[#out + 1] = "<age>|u147%:0:   :|u[seconds]|u200%:0:      :|uThe maximum age of restored chat" .. "\n";
     out[#out + 1] = "Example: /chatmessage tag short" .. "\n";

     d(table.concat(out,"\n"));
and for reference, dft is just a wrapper for d...

Code:
local function dft(line)
    if ( ( FarmersToolkit.FTREP == 1 ) and (type(line) ~= nil) )  then
                d(FarmersToolkit.FTChat .. line);
    end
end  -- function dft
Baertram - Following up on this. I'm unable to replicate the problem and realize I've also updated PChat, so I've completely lost the testing environment I had - which isn't a big loss as I am no longer convinced that PChat played a role, it may have been coincidence.

Since I'm unable to replicate, I've modified the original posting to clarify that PChat isn't a problem but will continue to check as time allows (which, looking at the next two months, won't be much). Sorry for the potential red herring. If I do find anything substantive, I'll let you know - with sufficient detail as to be useful.

I've also updated the Farmers Toolkit addon so the original problem is more or less addressed. However, I folded in the test code I used to demonstrate the problem, in case others want to try from their setups. The command "/ft ptest" will produce the output described in the original post. TL;DR - If there are spaces between "you" and "are here", things are working fine.

-V
  Reply With Quote
06/14/24, 06:52 AM   #15
Baertram
Super Moderator
 
Baertram's Avatar
WoWInterface Super Mod
AddOn Author - Click to view addons
Join Date: Mar 2014
Posts: 5,060
Thanks for the update, if you find the problem again and can relate it to pChat send me a PM and we will try to work something out.
  Reply With Quote

ESOUI » Developer Discussions » General Authoring Discussion » Perhaps the silliest question for this week?


Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off