IGraphics: How to draw text containing file path?

How to draw a text containing the path to the file so that if it does not fit completely into the rectangle, replace the characters in the middle of the line with an ellipsis, similar to how it is done in WinAPI using the DrawText function with the DT_PATH_ELLIPSIS flag?
This should be a cross platform solution. If anyone has a solution, please share.

1 Like

You can do it with a layer…

marque_text

The WDL_String::Ellipsize() method is also there for you

The WDL_String::Ellipsize() method is won’t fit. If string too long to fit, it should be shortened, e.g.
"C:\Users\Bla\Documents and Settings\Projects\Bla\bla.wav" should become
"C:\Users\Bla\...\Projects\Bla\bla.wav"

If you’d like to make PR for some kind of utility to do it how you think it should be done, i’ll take a look

This will do the trick (tested)

    auto FitText = [&](const IText &text, const char* str, const IRECT& bounds, bool breakInMiddle = false, const char* indicatorString = "...")
    {
      IRECT measureBounds;

      WDL_String textString;
      textString.Set(str);

      // Add indicator at the middle
      if (breakInMiddle && textString.GetLength() >= 2)
      {
        for (int i = 0; i < textString.GetLength() - 1; i++)
        {
          const int textHalfLength = (textString.GetLength() - i) / 2;

          WDL_String lowerText = textString;
          lowerText.SetLen(textHalfLength);

          WDL_String upperText = textString;
          upperText.DeleteSub(0, textString.GetLength() - textHalfLength);

          WDL_String combinedText = lowerText;
          combinedText.Append(indicatorString);
          combinedText.Append(upperText.Get());

          if (g.MeasureText(text, combinedText.Get(), measureBounds) <= bounds.W())
            return combinedText;
        }
      }

      // Add indicator at the end
      if (!breakInMiddle && textString.GetLength() >= 2)
      {
        for (int i = 0; i < textString.GetLength() - 1; i++)
        {
          WDL_String lowerText = textString;
          lowerText.SetLen(textString.GetLength() - i);

          WDL_String upperText;
          upperText.Set(indicatorString);

          WDL_String combinedText = lowerText;
          combinedText.Append(upperText.Get());

          if (g.MeasureText(text, combinedText.Get(), measureBounds) <= bounds.W())
            return combinedText;
        }
      }

      // If the above thing is not possible, try to clip the text
      for (int i = 0; i < textString.GetLength(); i++)
      {
        WDL_String clippedText = textString;
        clippedText.SetLen(textString.GetLength() - i);

        if (g.MeasureText(text, clippedText.Get(), measureBounds) <= bounds.W())
          return clippedText;
      }

      // If nothing is possible, return empty string
      textString.Set("");
      return textString;
    };

    IText textProps = IText(18, COLOR_WHITE, "Roboto-Regular");

    WDL_String textDraw = FitText(textProps, path.Get(), GetRECT());
    g.DrawText(textProps, textDraw.Get(), GetRECT());
1 Like

Solution found.:+1:t2:
To solve the problem, I made TFilePathBox a visual component class.

TFilePathBox

Here is the source code. Maybe someone will come in handy.
P.S. Didn’t check performance on macOS.

// interface:
class TFilePathBox : public IControl
{
private:
  IRECT fContentRect;
  WDL_String fCaption;
  float CaptionWidth();
public:
  TFilePathBox(const IRECT& bounds);
  void SetPath(const char* filename);
  void OnResize() override;
  void Draw(IGraphics& g) override;
};

// implementation:
bool IsLeadingLastUTF8Byte(const WDL_String str)
{
  const char* s = str.Get();
  const char* c = s + str.GetLength() - 1;
  auto first = (*c & 0x80) != 0;
  auto second = (*c & 0x40) != 0;
  return !first || second;
}

void RemoveLastChar(WDL_String& str)
{
  while (!IsLeadingLastUTF8Byte(str))
    str.DeleteSub(str.GetLength() - 1, 1);
  str.DeleteSub(str.GetLength() - 1, 1);
}


TFilePathBox::TFilePathBox(const IRECT& bounds):
  IControl(bounds, nullptr)
{
}

float TFilePathBox::CaptionWidth()
{
  IRECT r;
  GetUI()->MeasureText(mText, fCaption.Get(), r);
  return r.W();
}

void TFilePathBox::SetPath(const char* filename)
{
  mDirty = true;
  fCaption.Set(filename);
  // first check caption width
  if (CaptionWidth() <= fContentRect.W())
    return;
  // explode path to slices
#ifdef _WIN32
  const char sep[2] = "\\";
#else
  const char sep[2] = "/";
#endif
  // list of slices
  WDL_PtrList<char> list;
  // get the first token
  char* str = strdup(fCaption.Get());
  char* token = strtok(str, sep);
  // walk through other tokens 
  while (token)
  {
    list.Add(strdup(token));
    token = strtok(NULL, sep);
  }
  free(str);

  // make new fCaption
  bool truncate = false;
  while (CaptionWidth() > fContentRect.W())
  {
    int n = list.GetSize();
    if (n <= 2)
    {
      // 2 slice minimum - truncate needed
      truncate = true;
      break; 
    }
    // middle slice index
    n /= 2;
    list.Delete(n, free);
    // clear fCaption
    fCaption.SetLen(0);
    // build fCaption
    const int count = list.GetSize();
    for (int i = 0; i < count; ++i)
    {
      // add ... in middle
      if (i == n)
      {
        fCaption.Append("...");
        fCaption.Append(sep);
      }
      // add slice
      fCaption.Append(list.Get(i));

      // add directory separator if not is last slice
      if (i < count - 1)
        fCaption.Append(sep);      
    }
  } 
  list.Empty(true, free);

  // if truncate path needed
  if (truncate)
  {
    bool clip = false;
    float w = fContentRect.W();
    IRECT r;
    GetUI()->MeasureText(mText, "...", r);
    while (CaptionWidth() > w)
    {
      RemoveLastChar(fCaption);
      if (!clip)
      {
        clip = true;
        w -= r.W();
      }
    }
    if (clip)
      fCaption.Append("...");
  }
}

void TFilePathBox::OnResize()
{
  fContentRect = mRECT.GetPadded(-1);
}

void TFilePathBox::Draw(IGraphics& g)
{
  g.DrawText(mText, fCaption.Get(), fContentRect, &mBlend);
  g.DrawRect(COLOR_BLACK, mRECT, &mBlend);
}
2 Likes

Mine is a bit worse implementation as it doesn’t search for “/” in paths.

I have not yet tested this control so well with different paths to the file. So it might need to be improved. And also test the work on macOS.