/** \file
 * \brief WinUI Driver - Text Control
 *
 * See Copyright Notice in "iup.h"
 */

#include <cstdlib>
#include <cstring>
#include <cmath>

extern "C" {
#include "iup.h"
#include "iupcbs.h"
#include "iup_object.h"
#include "iup_attrib.h"
#include "iup_str.h"
#include "iup_array.h"
#include "iup_drvfont.h"
#include "iup_mask.h"
#include "iup_text.h"
#include "iup_image.h"
#include "iup_focus.h"
#include "iup_drvinfo.h"
}

#include "iupwinui_drv.h"

#include <winrt/Microsoft.UI.Text.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/Windows.Graphics.Imaging.h>

using namespace winrt;
using namespace Microsoft::UI;
using namespace Microsoft::UI::Xaml;
using namespace Microsoft::UI::Xaml::Controls;
using namespace Microsoft::UI::Xaml::Media;
using namespace Microsoft::UI::Xaml::Media::Imaging;
using namespace Microsoft::UI::Xaml::Input;
using namespace Microsoft::UI::Text;
using namespace Windows::Foundation;
using namespace Windows::Storage::Streams;
using namespace Microsoft::UI::Dispatching;


static ScrollViewer winuiTextFindScrollViewer(DependencyObject parent)
{
  int count = VisualTreeHelper::GetChildrenCount(parent);
  for (int i = 0; i < count; i++)
  {
    auto child = VisualTreeHelper::GetChild(parent, i);
    auto sv = child.try_as<ScrollViewer>();
    if (sv)
      return sv;

    sv = winuiTextFindScrollViewer(child);
    if (sv)
      return sv;
  }
  return nullptr;
}

static TextBox winuiFindTextBox(DependencyObject parent)
{
  int count = VisualTreeHelper::GetChildrenCount(parent);
  for (int i = 0; i < count; i++)
  {
    auto child = VisualTreeHelper::GetChild(parent, i);
    auto tb = child.try_as<TextBox>();
    if (tb)
      return tb;
    tb = winuiFindTextBox(child);
    if (tb)
      return tb;
  }
  return nullptr;
}

static bool winuiRemoveDeleteButton(DependencyObject parent)
{
  int count = VisualTreeHelper::GetChildrenCount(parent);
  for (int i = 0; i < count; i++)
  {
    auto child = VisualTreeHelper::GetChild(parent, i);
    auto fe = child.try_as<FrameworkElement>();
    if (fe && fe.Name() == L"DeleteButton")
    {
      auto panel = parent.try_as<Panel>();
      if (panel)
      {
        auto uiElem = child.try_as<UIElement>();
        if (uiElem)
        {
          uint32_t index;
          if (panel.Children().IndexOf(uiElem, index))
          {
            panel.Children().RemoveAt(index);
            return true;
          }
        }
      }
      return false;
    }
    if (winuiRemoveDeleteButton(child))
      return true;
  }
  return false;
}

static void winuiTextSetScrollBarArrowCursors(DependencyObject parent)
{
  using namespace Microsoft::UI::Xaml::Controls::Primitives;

  int count = VisualTreeHelper::GetChildrenCount(parent);
  for (int i = 0; i < count; i++)
  {
    auto child = VisualTreeHelper::GetChild(parent, i);
    auto sb = child.try_as<ScrollBar>();
    if (sb)
    {
      auto protectedUI = sb.try_as<IUIElementProtected>();
      if (protectedUI)
        protectedUI.ProtectedCursor(Microsoft::UI::Input::InputSystemCursor::Create(Microsoft::UI::Input::InputSystemCursorShape::Arrow));
    }
    else
      winuiTextSetScrollBarArrowCursors(child);
  }
}

static void winuiTextCallValueChanged(Ihandle* ih)
{
  if (ih->data->disable_callbacks)
    return;

  if (iupAttribGet(ih, "_IUPWINUI_IGNORE_VALUECHANGED"))
  {
    iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", NULL);
    return;
  }

  IFn cb = (IFn)IupGetCallback(ih, "VALUECHANGED_CB");
  if (cb)
  {
    int ret = cb(ih);
    if (ret == IUP_CLOSE)
      IupExitLoop();
  }
}

static void winuiTextBoxTextChanged(Ihandle* ih)
{
  if (ih->data->disable_callbacks)
  {
    iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", NULL);
    return;
  }

  winuiTextCallValueChanged(ih);
}

static void winuiTextBeforeTextChanging(Ihandle* ih, TextBox const& sender, TextBoxBeforeTextChangingEventArgs const& args)
{
  if (ih->data->disable_callbacks)
    return;

  IFnis cb = (IFnis)IupGetCallback(ih, "ACTION");
  if (!cb && !ih->data->mask && !ih->data->nc)
    return;

  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
    return;

  hstring newHStr = args.NewText();
  char* temp_new = iupwinuiHStringToString(newHStr);
  char* new_value = iupStrDup(temp_new ? temp_new : "");

  if (ih->data->nc && (int)strlen(new_value) > ih->data->nc)
  {
    free(new_value);
    args.Cancel(true);
    return;
  }

  if (ih->data->mask && iupMaskCheck((Imask*)ih->data->mask, new_value) == 0)
  {
    IFns fail_cb = (IFns)IupGetCallback(ih, "MASKFAIL_CB");
    if (fail_cb) fail_cb(ih, new_value);
    free(new_value);
    args.Cancel(true);
    return;
  }

  if (cb)
  {
    int oldLen = (int)aux->savedText.size();
    int newLen = (int)newHStr.size();

    const wchar_t* oldStr = aux->savedText.c_str();
    const wchar_t* newStr = newHStr.c_str();
    int insertPos = 0;
    while (insertPos < oldLen && insertPos < newLen && oldStr[insertPos] == newStr[insertPos])
      insertPos++;

    int key = 0;
    if (newLen == oldLen + 1 && insertPos < newLen)
    {
      wchar_t wch = newStr[insertPos];
      if (wch > 0 && wch < 128)
        key = (int)wch;
    }

    int cb_ret = cb(ih, key, new_value);
    if (cb_ret == IUP_IGNORE)
    {
      free(new_value);
      args.Cancel(true);
      return;
    }
    else if (cb_ret == IUP_CLOSE)
    {
      IupExitLoop();
      free(new_value);
      args.Cancel(true);
      return;
    }
    else if (cb_ret != 0 && key != 0 && cb_ret != IUP_DEFAULT && cb_ret != IUP_CONTINUE)
    {
      args.Cancel(true);

      int replaceCh = cb_ret;
      void* dq_ptr = iupwinuiGetDispatcherQueue();
      if (dq_ptr)
      {
        Windows::Foundation::IInspectable dq_obj{nullptr};
        winrt::copy_from_abi(dq_obj, dq_ptr);
        DispatcherQueue dq = dq_obj.as<DispatcherQueue>();

        dq.TryEnqueue([ih, replaceCh, insertPos]()
        {
          if (!iupObjectCheck(ih))
            return;

          IupWinUITextAux* a = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
          if (!a)
            return;

          TextBox tb = winuiGetHandle<TextBox>(ih);
          if (!tb)
            return;

          std::wstring text(a->savedText);
          text.insert(text.begin() + insertPos, (wchar_t)replaceCh);

          iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
          ih->data->disable_callbacks = 1;
          tb.Text(hstring(text));
          tb.SelectionStart(insertPos + 1);
          tb.SelectionLength(0);
          ih->data->disable_callbacks = 0;

          a->savedText = text;
        });
      }

      free(new_value);
      return;
    }
  }

  aux->savedText = std::wstring(newHStr.c_str(), newHStr.size());
  free(new_value);
}

static void winuiTextSpinValueChanged(Ihandle* ih, NumberBox const& nb, NumberBoxValueChangedEventArgs const& args)
{
  (void)nb;

  if (ih->data->disable_callbacks)
  {
    iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", NULL);
    return;
  }

  if (iupAttribGet(ih, "_IUPWINUI_IGNORE_VALUECHANGED"))
  {
    iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", NULL);
    return;
  }

  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);

  int newValue, oldValue;
  if (aux && !aux->spinauto && aux->spinStep > 0)
  {
    newValue = (int)round((args.NewValue() - aux->spinActualMin) / aux->spinStep);
    oldValue = (int)round((args.OldValue() - aux->spinActualMin) / aux->spinStep);
    int spinMin = iupAttribGetInt(ih, "_IUPWINUI_SPINMIN");
    int spinMax = iupAttribGetInt(ih, "_IUPWINUI_SPINMAX");
    if (newValue < spinMin) newValue = spinMin;
    if (newValue > spinMax) newValue = spinMax;
  }
  else
  {
    newValue = (int)args.NewValue();
    oldValue = (int)args.OldValue();
  }

  IFni cb = (IFni)IupGetCallback(ih, "SPIN_CB");
  if (cb)
  {
    int ret = cb(ih, newValue);
    if (ret == IUP_IGNORE)
    {
      NumberBox numberBox = winuiGetHandle<NumberBox>(ih);
      if (numberBox)
      {
        ih->data->disable_callbacks = 1;
        numberBox.Value(args.OldValue());
        ih->data->disable_callbacks = 0;
      }
      return;
    }
  }

  winuiTextCallValueChanged(ih);
}

static int winuiTextSetValueAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
    return 0;

  iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
  ih->data->disable_callbacks = 1;

  if (aux->isSpin)
  {
    NumberBox nb = winuiGetHandle<NumberBox>(ih);
    if (nb)
    {
      if (!aux->spinauto && aux->spinStep > 0)
      {
        double dval = 0;
        if (value)
          iupStrToDouble(value, &dval);
        nb.Value(dval);
      }
      else
      {
        int val = 0;
        if (value)
          iupStrToInt(value, &val);
        nb.Value((double)val);
      }
    }
  }
  else if (aux->isPassword)
  {
    PasswordBox pb = winuiGetHandle<PasswordBox>(ih);
    if (pb)
      pb.Password(iupwinuiStringToHString(value ? value : ""));
  }
  else if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      if (value && strchr(value, '\n') != NULL)
      {
        value = iupStrReturnStr(value);
        iupStrToMac((char*)value);
      }
      bool wasReadOnly = reb.IsReadOnly();
      if (wasReadOnly)
        reb.IsReadOnly(false);
      reb.Document().SetText(TextSetOptions::None, iupwinuiStringToHString(value ? value : ""));
      if (wasReadOnly)
        reb.IsReadOnly(true);
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Text(iupwinuiStringToHString(value ? value : ""));
    aux->savedText = iupwinuiStringToWString(value ? value : "");
  }

  ih->data->disable_callbacks = 0;

  return 0;
}

static char* winuiTextGetValueAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
    return NULL;

  if (aux->isSpin)
  {
    NumberBox nb = winuiGetHandle<NumberBox>(ih);
    if (nb)
    {
      if (!aux->spinauto && aux->spinStep > 0)
        return iupStrReturnDouble(nb.Value());
      else
        return iupStrReturnInt((int)nb.Value());
    }
  }
  else if (aux->isPassword)
  {
    PasswordBox pb = winuiGetHandle<PasswordBox>(ih);
    if (pb)
      return iupwinuiHStringToString(pb.Password());
  }
  else if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      hstring text;
      reb.Document().GetText(TextGetOptions::None, text);
      uint32_t len = text.size();
      if (len > 0 && text.c_str()[len - 1] == L'\r')
        return iupwinuiHStringToString(hstring(std::wstring_view(text.c_str(), len - 1)));
      return iupwinuiHStringToString(text);
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
    {
      char* str = iupwinuiHStringToString(tb.Text());
      return str ? str : (char*)"";
    }
  }

  return NULL;
}

static int winuiTextSetAppendAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin)
    return 0;

  if (!value)
    value = "";

  iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
  ih->data->disable_callbacks = 1;

  if (aux->isPassword)
  {
    PasswordBox pb = winuiGetHandle<PasswordBox>(ih);
    if (pb)
    {
      std::wstring newText(pb.Password().c_str());
      newText += iupwinuiStringToHString(value).c_str();
      pb.Password(hstring(newText));
    }
  }
  else if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      hstring currentText;
      reb.Document().GetText(TextGetOptions::None, currentText);

      std::wstring appendStr;
      if (ih->data->append_newline && currentText.size() > 0)
        appendStr = L"\r";
      appendStr += iupwinuiStringToHString(value).c_str();

      int32_t endPos = (int32_t)currentText.size();
      auto range = reb.Document().GetRange(endPos, endPos);
      range.SetText(TextSetOptions::None, hstring(appendStr));

      reb.UpdateLayout();
      ScrollViewer sv = winuiTextFindScrollViewer(reb);
      if (sv)
        sv.ChangeView(nullptr, sv.ScrollableHeight(), nullptr);
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
    {
      std::wstring newText(tb.Text().c_str());

      if (aux->isMultiline && ih->data->append_newline && !newText.empty())
        newText += L"\r";

      newText += iupwinuiStringToHString(value).c_str();
      tb.Text(hstring(newText));

      if (aux->isMultiline)
      {
        tb.UpdateLayout();
        ScrollViewer sv = winuiTextFindScrollViewer(tb);
        if (sv)
          sv.ChangeView(nullptr, sv.ScrollableHeight(), nullptr);
      }
    }
  }

  ih->data->disable_callbacks = 0;
  return 0;
}

static int winuiTextSetReadOnlyAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isPassword)
    return 0;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.IsReadOnly(iupStrBoolean(value));
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.IsReadOnly(iupStrBoolean(value));
  }

  return 0;
}

static char* winuiTextGetReadOnlyAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isPassword || aux->isSpin)
    return NULL;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      return iupStrReturnBoolean(reb.IsReadOnly());
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      return iupStrReturnBoolean(tb.IsReadOnly());
  }

  return NULL;
}

static char* winuiTextGetLineValueAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isPassword || aux->isSpin)
    return NULL;

  hstring fullText;
  int caretPos = 0;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (!reb) return NULL;
    hstring val;
    reb.Document().GetText(winrt::Microsoft::UI::Text::TextGetOptions::None, val);
    fullText = val;
    auto sel = reb.Document().Selection();
    caretPos = sel.StartPosition();
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (!tb) return NULL;
    fullText = tb.Text();
    caretPos = tb.SelectionStart();
  }

  std::wstring text(fullText.c_str(), fullText.size());
  if (text.empty())
    return NULL;

  int lineStart = caretPos;
  while (lineStart > 0 && text[lineStart - 1] != L'\r' && text[lineStart - 1] != L'\n')
    lineStart--;

  int lineEnd = caretPos;
  int len = (int)text.size();
  while (lineEnd < len && text[lineEnd] != L'\r' && text[lineEnd] != L'\n')
    lineEnd++;

  std::wstring line = text.substr(lineStart, lineEnd - lineStart);
  return iupwinuiHStringToString(hstring(line));
}

static int winuiTextSetActiveAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
    return 0;

  bool enabled = iupStrBoolean(value);

  if (aux->isSpin)
  {
    NumberBox nb = winuiGetHandle<NumberBox>(ih);
    if (nb)
      nb.IsEnabled(enabled);
  }
  else if (aux->isPassword)
  {
    PasswordBox pb = winuiGetHandle<PasswordBox>(ih);
    if (pb)
      pb.IsEnabled(enabled);
  }
  else if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.IsEnabled(enabled);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.IsEnabled(enabled);
  }

  return 0;
}

static const char* winuiTextFindLinkUrl(Ihandle* ih, int pos);

static int winuiTextGetPosFromPoint(Ihandle* ih, Windows::Foundation::Point point)
{
  RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
  if (!reb)
    return -1;

  try
  {
    auto doc = reb.Document();
    auto range = doc.GetRangeFromPoint(point, PointOptions::ClientCoordinates);
    if (!range)
      return -1;

    return range.StartPosition();
  }
  catch (...)
  {
    return -1;
  }
}

static void winuiTextLinkTapped(Ihandle* ih, Windows::Foundation::Point point)
{
  int pos = winuiTextGetPosFromPoint(ih, point);
  if (pos < 0)
    return;

  const char* url = winuiTextFindLinkUrl(ih, pos);
  if (url)
  {
    IFns cb = (IFns)IupGetCallback(ih, "LINK_CB");
    if (cb)
    {
      int ret = cb(ih, (char*)url);
      if (ret == IUP_CLOSE)
        IupExitLoop();
      else if (ret == IUP_DEFAULT)
        IupHelp(url);
    }
    else
      IupHelp(url);
  }
}

static void winuiTextLinkPointerMoved(Ihandle* ih, Windows::Foundation::Point clientPoint, Windows::Foundation::Point hostPoint)
{
  using namespace Microsoft::UI::Input;

  RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
  if (!reb)
    return;

  int pos = winuiTextGetPosFromPoint(ih, clientPoint);
  const char* url = (pos >= 0) ? winuiTextFindLinkUrl(ih, pos) : NULL;
  InputSystemCursorShape shape = url ? InputSystemCursorShape::Hand : InputSystemCursorShape::IBeam;

  auto elements = VisualTreeHelper::FindElementsInHostCoordinates(hostPoint, reb.as<UIElement>());
  for (auto const& elem : elements)
  {
    auto protectedUI = elem.try_as<Microsoft::UI::Xaml::IUIElementProtected>();
    if (protectedUI)
    {
      protectedUI.ProtectedCursor(InputSystemCursor::Create(shape));
      break;
    }
  }
}

static int winuiTextMapMethod(Ihandle* ih)
{
  IupWinUITextAux* aux = new IupWinUITextAux();
  int isPassword = iupAttribGetBoolean(ih, "PASSWORD");
  int isMultiline = ih->data->is_multiline;
  int isSpin = !isMultiline && !isPassword && iupAttribGetBoolean(ih, "SPIN");

  aux->isPassword = isPassword ? true : false;
  aux->isMultiline = isMultiline ? true : false;
  aux->isSpin = isSpin ? true : false;
  aux->spinauto = iupAttribGetBoolean(ih, "SPINAUTO") ? true : false;

  const char* value = iupAttribGet(ih, "VALUE");

  if (aux->isSpin)
  {
    NumberBox nb;
    nb.HorizontalAlignment(HorizontalAlignment::Left);
    nb.VerticalAlignment(VerticalAlignment::Top);
    nb.MinWidth(0);
    nb.MinHeight(0);
    nb.SpinButtonPlacementMode(NumberBoxSpinButtonPlacementMode::Inline);

    int spinMin = iupAttribGetInt(ih, "SPINMIN");
    int spinMax = iupAttribGetInt(ih, "SPINMAX");
    int spinInc = iupAttribGetInt(ih, "SPININC");
    int spinValue = iupAttribGetInt(ih, "SPINVALUE");

    if (spinMax == 0 && spinMin == 0)
      spinMax = 100;
    if (spinInc == 0)
      spinInc = 1;

    if (!aux->spinauto)
    {
      double actualMin = iupAttribGetDouble(ih, "_IUPWINUI_MASKFLOAT_MIN");
      double actualMax = iupAttribGetDouble(ih, "_IUPWINUI_MASKFLOAT_MAX");
      if (actualMax > actualMin)
      {

        int spinRange = spinMax - spinMin;
        if (spinRange <= 0) spinRange = 100;
        double step = (actualMax - actualMin) / (double)spinRange * (double)spinInc;

        aux->spinStep = step;
        aux->spinActualMin = actualMin;

        iupAttribSetInt(ih, "_IUPWINUI_SPINMIN", spinMin);
        iupAttribSetInt(ih, "_IUPWINUI_SPINMAX", spinMax);

        nb.Minimum(actualMin);
        nb.Maximum(actualMax);
        nb.SmallChange(step);
        nb.LargeChange(step);

        {
          int precision = IupGetInt(NULL, "DEFAULTPRECISION");
          if (precision <= 0) precision = 2;
          using IFmtOpts = winrt::Windows::Globalization::NumberFormatting::INumberFormatterOptions;
          auto opts = nb.NumberFormatter().as<IFmtOpts>();
          reinterpret_cast<winrt::impl::abi<IFmtOpts>::type*>(winrt::get_abi(opts))->put_FractionDigits(precision);
        }

        double dval = 0;
        if (value)
          iupStrToDouble(value, &dval);
        else
          dval = (double)spinValue * step + actualMin;
        nb.Value(dval);

        goto spin_events;
      }
    }

    nb.Minimum((double)spinMin);
    nb.Maximum((double)spinMax);
    nb.SmallChange((double)spinInc);
    nb.LargeChange((double)spinInc);

    if (value)
    {
      int val = 0;
      iupStrToInt(value, &val);
      nb.Value((double)val);
    }
    else
      nb.Value((double)spinValue);

    spin_events:

    aux->valueChangedToken = nb.ValueChanged([ih](NumberBox const& sender, NumberBoxValueChangedEventArgs const& args) {
      winuiTextSpinValueChanged(ih, sender, args);
    });

    aux->gotFocusToken = nb.GotFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallGetFocusCb(ih);
    });

    aux->lostFocusToken = nb.LostFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallKillFocusCb(ih);
    });

    aux->keyDownToken = nb.PreviewKeyDown([ih](IInspectable const&, KeyRoutedEventArgs const& args) {
      if (!iupwinuiKeyEvent(ih, (int)args.Key(), 1))
        args.Handled(true);
    });

    iupwinuiUpdateControlFont(ih, nb);

    Canvas parentCanvas = iupwinuiGetParentCanvas(ih);
    if (parentCanvas)
      parentCanvas.Children().Append(nb);

    nb.Loaded([](IInspectable const& sender, RoutedEventArgs const&) {
      TextBox tb = winuiFindTextBox(sender.as<DependencyObject>());
      if (tb)
      {
        tb.ApplyTemplate();
        winuiRemoveDeleteButton(tb.as<DependencyObject>());
      }
    });

    winuiStoreHandle(ih, nb);
  }
  else if (aux->isPassword)
  {
    PasswordBox pb = PasswordBox();
    pb.HorizontalAlignment(HorizontalAlignment::Left);
    pb.VerticalAlignment(VerticalAlignment::Top);
    pb.MinWidth(0);
    pb.MinHeight(0);

    if (value)
      pb.Password(iupwinuiStringToHString(value));

    aux->textChangedToken = pb.PasswordChanged([ih](IInspectable const&, RoutedEventArgs const&) {
      winuiTextCallValueChanged(ih);
    });

    aux->gotFocusToken = pb.GotFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallGetFocusCb(ih);
    });

    aux->lostFocusToken = pb.LostFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallKillFocusCb(ih);
    });

    aux->keyDownToken = pb.PreviewKeyDown([ih](IInspectable const&, KeyRoutedEventArgs const& args) {
      if (!iupwinuiKeyEvent(ih, (int)args.Key(), 1))
        args.Handled(true);
    });

    iupwinuiUpdateControlFont(ih, pb);

    Canvas parentCanvas = iupwinuiGetParentCanvas(ih);
    if (parentCanvas)
      parentCanvas.Children().Append(pb);

    winuiStoreHandle(ih, pb);
  }
  else if (ih->data->has_formatting && isMultiline)
  {
    RichEditBox reb;
    reb.HorizontalAlignment(HorizontalAlignment::Left);
    reb.VerticalAlignment(VerticalAlignment::Top);
    reb.MinWidth(0);
    reb.MinHeight(0);
    reb.SelectionFlyout(nullptr);
    reb.ContextFlyout(nullptr);

    if (iupAttribGetBoolean(ih, "WORDWRAP"))
    {
      reb.TextWrapping(TextWrapping::Wrap);
      ih->data->sb &= ~IUP_SB_HORIZ;
    }
    else
      reb.TextWrapping(TextWrapping::NoWrap);

    if (ih->data->sb & IUP_SB_HORIZ)
    {
      if (iupAttribGetBoolean(ih, "AUTOHIDE"))
        ScrollViewer::SetHorizontalScrollBarVisibility(reb, ScrollBarVisibility::Auto);
      else
        ScrollViewer::SetHorizontalScrollBarVisibility(reb, ScrollBarVisibility::Visible);
    }
    else
      ScrollViewer::SetHorizontalScrollBarVisibility(reb, ScrollBarVisibility::Disabled);

    if (ih->data->sb & IUP_SB_VERT)
    {
      if (iupAttribGetBoolean(ih, "AUTOHIDE"))
        ScrollViewer::SetVerticalScrollBarVisibility(reb, ScrollBarVisibility::Auto);
      else
        ScrollViewer::SetVerticalScrollBarVisibility(reb, ScrollBarVisibility::Visible);
    }
    else
      ScrollViewer::SetVerticalScrollBarVisibility(reb, ScrollBarVisibility::Disabled);

    aux->textChangedToken = reb.TextChanged([ih](IInspectable const&, RoutedEventArgs const&) {
      winuiTextCallValueChanged(ih);
    });

    aux->gotFocusToken = reb.GotFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallGetFocusCb(ih);
    });

    aux->lostFocusToken = reb.LostFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallKillFocusCb(ih);
    });

    aux->keyDownToken = reb.PreviewKeyDown([ih](IInspectable const&, KeyRoutedEventArgs const& args) {
      if (!iupwinuiKeyEvent(ih, (int)args.Key(), 1))
        args.Handled(true);
    });

    reb.AddHandler(UIElement::PointerReleasedEvent(), winrt::box_value(
      PointerEventHandler([ih](IInspectable const& sender, PointerRoutedEventArgs const& args) {
        auto pt = args.GetCurrentPoint(sender.as<UIElement>());
        if (pt.Properties().PointerUpdateKind() == Microsoft::UI::Input::PointerUpdateKind::LeftButtonReleased)
          winuiTextLinkTapped(ih, pt.Position());
      })), true);

    reb.AddHandler(UIElement::PointerMovedEvent(), winrt::box_value(
      PointerEventHandler([ih](IInspectable const& sender, PointerRoutedEventArgs const& args) {
        auto clientPt = args.GetCurrentPoint(sender.as<UIElement>()).Position();
        auto hostPt = args.GetCurrentPoint(nullptr).Position();
        winuiTextLinkPointerMoved(ih, clientPt, hostPt);
      })), true);

    iupwinuiUpdateControlFont(ih, reb);

    reb.ClearValue(Control::FontSizeProperty());
    reb.ClearValue(Control::FontFamilyProperty());

    {
      auto doc = reb.Document();
      auto dcf = doc.GetDefaultCharacterFormat();
      int fontSize = 0;
      char* szStr = iupGetFontSizeAttrib(ih);
      if (szStr && iupStrToInt(szStr, &fontSize))
      {
        if (fontSize < 0)
          fontSize = (-fontSize) * 72 / (int)iupdrvGetScreenDpi();
        dcf.Size((float)fontSize);
      }
      char* fontFace = iupGetFontFaceAttrib(ih);
      if (fontFace)
        dcf.Name(iupwinuiStringToHString(fontFace));
      doc.SetDefaultCharacterFormat(dcf);
    }

    Canvas parentCanvas = iupwinuiGetParentCanvas(ih);
    if (parentCanvas)
      parentCanvas.Children().Append(reb);

    reb.ApplyTemplate();

    reb.Loaded([](IInspectable const& sender, RoutedEventArgs const&) {
      winuiTextSetScrollBarArrowCursors(sender.as<DependencyObject>());
    });

    winuiStoreHandle(ih, reb);
    aux->isFormatted = true;
  }
  else
  {
    TextBox tb = TextBox();
    tb.HorizontalAlignment(HorizontalAlignment::Left);
    tb.VerticalAlignment(VerticalAlignment::Top);
    tb.MinWidth(0);
    tb.MinHeight(0);

    if (aux->isMultiline)
    {
      tb.AcceptsReturn(true);

      if (iupAttribGetBoolean(ih, "WORDWRAP"))
      {
        tb.TextWrapping(TextWrapping::Wrap);
        ih->data->sb &= ~IUP_SB_HORIZ;
      }
      else
        tb.TextWrapping(TextWrapping::NoWrap);

      if (ih->data->sb & IUP_SB_HORIZ)
      {
        if (iupAttribGetBoolean(ih, "AUTOHIDE"))
          ScrollViewer::SetHorizontalScrollBarVisibility(tb, ScrollBarVisibility::Auto);
        else
          ScrollViewer::SetHorizontalScrollBarVisibility(tb, ScrollBarVisibility::Visible);
      }
      else
        ScrollViewer::SetHorizontalScrollBarVisibility(tb, ScrollBarVisibility::Disabled);

      if (ih->data->sb & IUP_SB_VERT)
      {
        if (iupAttribGetBoolean(ih, "AUTOHIDE"))
          ScrollViewer::SetVerticalScrollBarVisibility(tb, ScrollBarVisibility::Auto);
        else
          ScrollViewer::SetVerticalScrollBarVisibility(tb, ScrollBarVisibility::Visible);
      }
      else
        ScrollViewer::SetVerticalScrollBarVisibility(tb, ScrollBarVisibility::Disabled);

      tb.Loaded([](IInspectable const& sender, RoutedEventArgs const&) {
        winuiTextSetScrollBarArrowCursors(sender.as<DependencyObject>());
      });
    }

    if (!iupAttribGetBoolean(ih, "BORDER"))
    {
      tb.BorderThickness(Thickness{0, 0, 0, 0});
      tb.Padding(Thickness{0, 0, 0, 0});
    }

    int isReadOnly = iupAttribGetBoolean(ih, "READONLY");
    tb.IsReadOnly(isReadOnly ? true : false);

    if (value)
    {
      tb.Text(iupwinuiStringToHString(value));
      aux->savedText = iupwinuiStringToWString(value);
    }

    aux->beforeTextChangingToken = tb.BeforeTextChanging([ih](TextBox const& sender, TextBoxBeforeTextChangingEventArgs const& args) {
      winuiTextBeforeTextChanging(ih, sender, args);
    });

    aux->textChangedToken = tb.TextChanged([ih](IInspectable const&, TextChangedEventArgs const&) {
      winuiTextBoxTextChanged(ih);
    });

    aux->gotFocusToken = tb.GotFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallGetFocusCb(ih);
    });

    aux->lostFocusToken = tb.LostFocus([ih](IInspectable const&, RoutedEventArgs const&) {
      iupCallKillFocusCb(ih);
    });

    aux->keyDownToken = tb.PreviewKeyDown([ih](IInspectable const&, KeyRoutedEventArgs const& args) {
      if (!iupwinuiKeyEvent(ih, (int)args.Key(), 1))
        args.Handled(true);
    });

    iupwinuiUpdateControlFont(ih, tb);

    Canvas parentCanvas = iupwinuiGetParentCanvas(ih);
    if (parentCanvas)
      parentCanvas.Children().Append(tb);

    winuiStoreHandle(ih, tb);
  }

  winuiSetAux(ih, IUPWINUI_TEXT_AUX, aux);

  if (ih->data->has_formatting)
  {
    iupAttribSet(ih, "_IUPWINUI_FONTUPDATECHECK", "1");
    iupUpdateFontAttrib(ih);
  }

  iupTextUpdateFormatTags(ih);

  if (IupGetCallback(ih, "DROPFILES_CB"))
    iupAttribSet(ih, "DROPFILESTARGET", "YES");

  return IUP_NOERROR;
}

static void winuiTextUnMapMethod(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);

  if (ih->handle && aux)
  {
    if (aux->isSpin)
    {
      NumberBox nb = winuiGetHandle<NumberBox>(ih);
      if (nb)
      {
        if (aux->valueChangedToken)
          nb.ValueChanged(aux->valueChangedToken);
        if (aux->gotFocusToken)
          nb.GotFocus(aux->gotFocusToken);
        if (aux->lostFocusToken)
          nb.LostFocus(aux->lostFocusToken);
        if (aux->keyDownToken)
          nb.PreviewKeyDown(aux->keyDownToken);
      }
    }
    else if (aux->isPassword)
    {
      PasswordBox pb = winuiGetHandle<PasswordBox>(ih);
      if (pb)
      {
        if (aux->textChangedToken)
          pb.PasswordChanged(aux->textChangedToken);
        if (aux->gotFocusToken)
          pb.GotFocus(aux->gotFocusToken);
        if (aux->lostFocusToken)
          pb.LostFocus(aux->lostFocusToken);
        if (aux->keyDownToken)
          pb.PreviewKeyDown(aux->keyDownToken);
      }
    }
    else if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
      {
        if (aux->textChangedToken)
          reb.TextChanged(aux->textChangedToken);
        if (aux->gotFocusToken)
          reb.GotFocus(aux->gotFocusToken);
        if (aux->lostFocusToken)
          reb.LostFocus(aux->lostFocusToken);
        if (aux->keyDownToken)
          reb.PreviewKeyDown(aux->keyDownToken);
        if (aux->tappedToken)
          reb.Tapped(aux->tappedToken);
      }
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
      {
        if (aux->beforeTextChangingToken)
          tb.BeforeTextChanging(aux->beforeTextChangingToken);
        if (aux->textChangedToken)
          tb.TextChanged(aux->textChangedToken);
        if (aux->gotFocusToken)
          tb.GotFocus(aux->gotFocusToken);
        if (aux->lostFocusToken)
          tb.LostFocus(aux->lostFocusToken);
        if (aux->keyDownToken)
          tb.PreviewKeyDown(aux->keyDownToken);
      }
    }
  }

  bool isSpin = aux ? aux->isSpin : false;
  bool isPassword = aux ? aux->isPassword : false;
  bool isFormatted = aux ? aux->isFormatted : false;
  winuiFreeAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);

  if (isSpin)
    winuiReleaseHandle<NumberBox>(ih);
  else if (isPassword)
    winuiReleaseHandle<PasswordBox>(ih);
  else if (isFormatted)
    winuiReleaseHandle<RichEditBox>(ih);
  else
    winuiReleaseHandle<TextBox>(ih);
}

static int winuiTextSetSpinMinAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !aux->isSpin)
    return 1;

  int val = 0;
  iupStrToInt(value, &val);

  if (!aux->spinauto && aux->spinStep > 0)
    iupAttribSetInt(ih, "_IUPWINUI_SPINMIN", val);
  else
  {
    NumberBox nb = winuiGetHandle<NumberBox>(ih);
    if (nb)
      nb.Minimum((double)val);
  }

  return 1;
}

static int winuiTextSetSpinMaxAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !aux->isSpin)
    return 1;

  int val = 100;
  iupStrToInt(value, &val);

  if (!aux->spinauto && aux->spinStep > 0)
    iupAttribSetInt(ih, "_IUPWINUI_SPINMAX", val);
  else
  {
    NumberBox nb = winuiGetHandle<NumberBox>(ih);
    if (nb)
      nb.Maximum((double)val);
  }

  return 1;
}

static int winuiTextSetSpinIncAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !aux->isSpin)
    return 1;

  if (!aux->spinauto && aux->spinStep > 0)
    return 1;

  int val = 1;
  iupStrToInt(value, &val);

  NumberBox nb = winuiGetHandle<NumberBox>(ih);
  if (nb)
  {
    nb.SmallChange((double)val);
    nb.LargeChange((double)val);
  }

  return 1;
}

static int winuiTextSetSpinValueAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !aux->isSpin)
    return 1;

  int val = 0;
  iupStrToInt(value, &val);

  if (!aux->spinauto && aux->spinStep > 0)
    return 1;

  NumberBox nb = winuiGetHandle<NumberBox>(ih);
  if (nb)
  {
    iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
    ih->data->disable_callbacks = 1;
    nb.Value((double)val);
    ih->data->disable_callbacks = 0;
  }

  return 1;
}

static char* winuiTextGetSpinValueAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !aux->isSpin)
    return NULL;

  NumberBox nb = winuiGetHandle<NumberBox>(ih);
  if (nb)
  {
    if (!aux->spinauto && aux->spinStep > 0)
      return iupStrReturnInt((int)round((nb.Value() - aux->spinActualMin) / aux->spinStep));
    else
      return iupStrReturnInt((int)nb.Value());
  }

  return NULL;
}

static float winui_multiline_border_height = -1;
static int winui_border_x = -1;
static int winui_border_y = -1;

static void winuiTextMeasureBorderMetrics(void)
{
  if (winui_border_x >= 0)
    return;

  float border_v = 0;
  float border_h = 0;
  auto resources = Application::Current().Resources();

  auto borderKey = box_value(L"TextControlBorderThemeThickness");
  if (resources.HasKey(borderKey))
  {
    auto bt = unbox_value<Thickness>(resources.Lookup(borderKey));
    border_v += (float)(bt.Top + bt.Bottom);
    border_h += (float)(bt.Left + bt.Right);
  }

  auto paddingKey = box_value(L"TextControlThemePadding");
  if (resources.HasKey(paddingKey))
  {
    auto pd = unbox_value<Thickness>(resources.Lookup(paddingKey));
    border_v += (float)(pd.Top + pd.Bottom);
    border_h += (float)(pd.Left + pd.Right);
  }

  winui_multiline_border_height = border_v;
  if (winui_multiline_border_height < 2) winui_multiline_border_height = 6;

  winui_border_x = (int)ceil(border_h);
  winui_border_y = (int)ceil(border_v);
  if (winui_border_x < 6) winui_border_x = 6;
  if (winui_border_y < 6) winui_border_y = 6;
}

extern "C" IUP_SDK_API void iupdrvTextAddBorders(Ihandle* ih, int* x, int* y)
{
  winuiTextMeasureBorderMetrics();

  if (ih->data && ih->data->is_multiline)
  {
    int visiblelines = iupAttribGetInt(ih, "VISIBLELINES");

    *x += winui_border_x;

    if (visiblelines > 0)
    {
      int char_height;
      iupdrvFontGetCharSize(ih, NULL, &char_height);

      float line_height_f = iupwinuiFontGetMultilineLineHeightF(ih);

      *y -= char_height * visiblelines;
      *y += (int)ceil(winui_multiline_border_height + line_height_f * visiblelines);
    }
    else
    {
      *y += (int)ceil(winui_multiline_border_height);
    }
  }
  else
  {
    *x += winui_border_x;
    *y += winui_border_y;
  }
}

extern "C" IUP_SDK_API void iupdrvTextAddSpin(Ihandle* ih, int* w, int h)
{
  (void)ih;
  (void)h;
  *w += 64;  /* Two spin buttons at 32px each (from NumberBoxSpinButtonStyle MinWidth) */
}

static int winuiTextLinColToPos(ITextDocument const& doc, int lin, int col);

static int winuiTextStringLinColToPos(const wchar_t* text, int textLen, int lin, int col)
{
  int currentLine = 1;
  int lineStart = 0;

  while (currentLine < lin && lineStart < textLen)
  {
    if (text[lineStart] == L'\r')
      currentLine++;
    lineStart++;
  }

  int lineLen = 0;
  int p = lineStart;
  while (p < textLen && text[p] != L'\r')
  {
    lineLen++;
    p++;
  }

  col--;
  if (col < 0) col = 0;
  if (col > lineLen) col = lineLen;

  return lineStart + col;
}

static void winuiTextStringPosToLinCol(const wchar_t* text, int textLen, int pos, int* lin, int* col)
{
  if (pos > textLen)
    pos = textLen;
  if (pos < 0)
    pos = 0;

  int currentLine = 1;
  int lineStart = 0;

  for (int i = 0; i < pos; i++)
  {
    if (text[i] == L'\r')
    {
      currentLine++;
      lineStart = i + 1;
    }
  }

  *lin = currentLine;
  *col = pos - lineStart + 1;
}

extern "C" IUP_SDK_API void iupdrvTextConvertLinColToPos(Ihandle* ih, int lin, int col, int* pos)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
  {
    *pos = 0;
    return;
  }

  if (!ih->data->is_multiline)
  {
    *pos = col - 1;
    if (*pos < 0) *pos = 0;
    return;
  }

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      *pos = winuiTextLinColToPos(reb.Document(), lin, col);
    else
      *pos = 0;
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
    {
      hstring text = tb.Text();
      *pos = winuiTextStringLinColToPos(text.c_str(), (int)text.size(), lin, col);
    }
    else
      *pos = 0;
  }
}

extern "C" IUP_SDK_API void iupdrvTextConvertPosToLinCol(Ihandle* ih, int pos, int* lin, int* col)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
  {
    *lin = 0;
    *col = 0;
    return;
  }

  if (!ih->data->is_multiline)
  {
    *lin = 1;
    *col = pos + 1;
    return;
  }

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      hstring text;
      reb.Document().GetText(TextGetOptions::None, text);
      winuiTextStringPosToLinCol(text.c_str(), (int)text.size(), pos, lin, col);
    }
    else
    {
      *lin = 0;
      *col = 0;
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
    {
      hstring text = tb.Text();
      winuiTextStringPosToLinCol(text.c_str(), (int)text.size(), pos, lin, col);
    }
    else
    {
      *lin = 0;
      *col = 0;
    }
  }
}

extern "C" IUP_SDK_API void* iupdrvTextAddFormatTagStartBulk(Ihandle* ih)
{
  (void)ih;
  return NULL;
}

extern "C" IUP_SDK_API void iupdrvTextAddFormatTagStopBulk(Ihandle* ih, void* state)
{
  (void)ih;
  (void)state;
}

static int winuiTextLinColToPos(ITextDocument const& doc, int lin, int col)
{
  hstring text;
  doc.GetText(TextGetOptions::None, text);
  std::wstring_view wtext(text.c_str(), text.size());
  int textLen = (int)wtext.size();

  int currentLine = 1;
  int lineStart = 0;

  while (currentLine < lin && lineStart < textLen)
  {
    if (wtext[lineStart] == L'\r')
      currentLine++;
    lineStart++;
  }

  int lineLen = 0;
  int p = lineStart;
  while (p < textLen && wtext[p] != L'\r')
  {
    lineLen++;
    p++;
  }

  col--;
  if (col < 0) col = 0;
  if (col > lineLen) col = lineLen;

  return lineStart + col;
}

static void winuiTextParseCharacterFormat(Ihandle* formattag, ITextRange const& range)
{
  auto cf = range.CharacterFormat();
  bool changed = false;
  char* val;

  val = iupAttribGet(formattag, "BGCOLOR");
  if (val)
  {
    unsigned char r, g, b;
    if (iupStrToRGB(val, &r, &g, &b))
    {
      Windows::UI::Color color;
      color.A = 255; color.R = r; color.G = g; color.B = b;
      cf.BackgroundColor(color);
      changed = true;
    }
  }

  val = iupAttribGet(formattag, "FGCOLOR");
  if (val)
  {
    unsigned char r, g, b;
    if (iupStrToRGB(val, &r, &g, &b))
    {
      Windows::UI::Color color;
      color.A = 255; color.R = r; color.G = g; color.B = b;
      cf.ForegroundColor(color);
      changed = true;
    }
  }

  val = iupAttribGet(formattag, "ITALIC");
  if (val)
  {
    cf.Italic(iupStrBoolean(val) ? FormatEffect::On : FormatEffect::Off);
    changed = true;
  }

  val = iupAttribGet(formattag, "STRIKEOUT");
  if (val)
  {
    cf.Strikethrough(iupStrBoolean(val) ? FormatEffect::On : FormatEffect::Off);
    changed = true;
  }

  val = iupAttribGet(formattag, "UNDERLINE");
  if (val)
  {
    if (iupStrEqualNoCase(val, "SINGLE"))
      cf.Underline(UnderlineType::Single);
    else if (iupStrEqualNoCase(val, "DOUBLE"))
      cf.Underline(UnderlineType::Double);
    else if (iupStrEqualNoCase(val, "DOTTED"))
      cf.Underline(UnderlineType::Dotted);
    else
      cf.Underline(UnderlineType::None);
    changed = true;
  }

  val = iupAttribGet(formattag, "WEIGHT");
  if (val)
  {
    int weight = 400;
    if (iupStrEqualNoCase(val, "EXTRALIGHT")) weight = 200;
    else if (iupStrEqualNoCase(val, "LIGHT")) weight = 300;
    else if (iupStrEqualNoCase(val, "NORMAL")) weight = 400;
    else if (iupStrEqualNoCase(val, "SEMIBOLD")) weight = 600;
    else if (iupStrEqualNoCase(val, "BOLD")) weight = 700;
    else if (iupStrEqualNoCase(val, "EXTRABOLD")) weight = 800;
    else if (iupStrEqualNoCase(val, "HEAVY")) weight = 900;
    cf.Weight(weight);
    changed = true;
  }

  val = iupAttribGet(formattag, "PROTECTED");
  if (val)
  {
    cf.ProtectedText(iupStrBoolean(val) ? FormatEffect::On : FormatEffect::Off);
    changed = true;
  }

  val = iupAttribGet(formattag, "SMALLCAPS");
  if (val)
  {
    cf.SmallCaps(iupStrBoolean(val) ? FormatEffect::On : FormatEffect::Off);
    changed = true;
  }

  val = iupAttribGet(formattag, "FONTSIZE");
  if (val)
  {
    int size = 0;
    if (iupStrToInt(val, &size))
    {
      float fontSize;
      if (size > 0)
        fontSize = (float)size;
      else
        fontSize = (float)(-size) * 72.0f / (float)iupdrvGetScreenDpi();

      char* fontscale = iupAttribGet(formattag, "FONTSCALE");
      if (fontscale)
      {
        double scale = 0;
        if (iupStrEqualNoCase(fontscale, "XX-SMALL")) scale = 0.5787;
        else if (iupStrEqualNoCase(fontscale, "X-SMALL")) scale = 0.6944;
        else if (iupStrEqualNoCase(fontscale, "SMALL")) scale = 0.8333;
        else if (iupStrEqualNoCase(fontscale, "MEDIUM")) scale = 1.0;
        else if (iupStrEqualNoCase(fontscale, "LARGE")) scale = 1.2;
        else if (iupStrEqualNoCase(fontscale, "X-LARGE")) scale = 1.44;
        else if (iupStrEqualNoCase(fontscale, "XX-LARGE")) scale = 1.728;
        else iupStrToDouble(fontscale, &scale);

        if (scale > 0)
          fontSize = (float)(fontSize * scale);
      }

      cf.Size(fontSize);
      changed = true;
    }
  }

  val = iupAttribGet(formattag, "FONTFACE");
  if (val)
  {
    const char* mapped_name = iupFontGetWinName(val);
    if (mapped_name)
      cf.Name(iupwinuiStringToHString(mapped_name));
    else
      cf.Name(iupwinuiStringToHString(val));
    changed = true;
  }

  val = iupAttribGet(formattag, "LINK");
  if (val)
  {
    if (!iupAttribGet(formattag, "FGCOLOR"))
    {
      Windows::UI::Color blue;
      blue.A = 255; blue.R = 0; blue.G = 0; blue.B = 255;
      cf.ForegroundColor(blue);
    }

    if (!iupAttribGet(formattag, "UNDERLINE"))
      cf.Underline(UnderlineType::Single);

    changed = true;
  }

  if (changed)
    range.CharacterFormat(cf);
}

static void winuiTextParseParagraphFormat(Ihandle* formattag, ITextRange const& range)
{
  auto pf = range.ParagraphFormat();
  bool changed = false;
  char* val;
  float pixelToPoint = 72.0f / (float)iupdrvGetScreenDpi();

  val = iupAttribGet(formattag, "UNITS");
  if (val && iupStrEqualNoCase(val, "TWIPS"))
    pixelToPoint = 1.0f / 20.0f;

  val = iupAttribGet(formattag, "ALIGNMENT");
  if (val)
  {
    if (iupStrEqualNoCase(val, "JUSTIFY"))
      pf.Alignment(ParagraphAlignment::Justify);
    else if (iupStrEqualNoCase(val, "RIGHT"))
      pf.Alignment(ParagraphAlignment::Right);
    else if (iupStrEqualNoCase(val, "CENTER"))
      pf.Alignment(ParagraphAlignment::Center);
    else
      pf.Alignment(ParagraphAlignment::Left);
    changed = true;
  }

  val = iupAttribGet(formattag, "INDENT");
  if (val)
  {
    int ival = 0;
    if (iupStrToInt(val, &ival))
    {
      float leftIndent = (float)ival * pixelToPoint;

      char* indentRight = iupAttribGet(formattag, "INDENTRIGHT");
      int irval = ival;
      if (indentRight)
        iupStrToInt(indentRight, &irval);
      float rightIndent = (float)irval * pixelToPoint;

      float firstLineIndent = 0;
      char* indentOffset = iupAttribGet(formattag, "INDENTOFFSET");
      if (indentOffset)
      {
        int ioval = 0;
        iupStrToInt(indentOffset, &ioval);
        firstLineIndent = (float)ioval * pixelToPoint;
      }

      pf.SetIndents(firstLineIndent, leftIndent, rightIndent);
      changed = true;
    }
  }

  val = iupAttribGet(formattag, "SPACEBEFORE");
  if (val)
  {
    int ival = 0;
    if (iupStrToInt(val, &ival))
    {
      pf.SpaceBefore((float)ival * pixelToPoint);
      changed = true;
    }
  }

  val = iupAttribGet(formattag, "SPACEAFTER");
  if (val)
  {
    int ival = 0;
    if (iupStrToInt(val, &ival))
    {
      pf.SpaceAfter((float)ival * pixelToPoint);
      changed = true;
    }
  }

  val = iupAttribGet(formattag, "LINESPACING");
  if (val)
  {
    if (iupStrEqualNoCase(val, "SINGLE"))
      pf.SetLineSpacing(LineSpacingRule::Multiple, 1.0f);
    else if (iupStrEqualNoCase(val, "ONEHALF"))
      pf.SetLineSpacing(LineSpacingRule::Multiple, 1.5f);
    else if (iupStrEqualNoCase(val, "DOUBLE"))
      pf.SetLineSpacing(LineSpacingRule::Multiple, 2.0f);
    else
    {
      int ival = 0;
      if (iupStrToInt(val, &ival))
        pf.SetLineSpacing(LineSpacingRule::AtLeast, (float)ival * pixelToPoint);
    }
    changed = true;
  }

  val = iupAttribGet(formattag, "NUMBERING");
  if (val)
  {
    if (iupStrEqualNoCase(val, "BULLET"))
      pf.ListType(MarkerType::Bullet);
    else if (iupStrEqualNoCase(val, "ARABIC"))
      pf.ListType(MarkerType::Arabic);
    else if (iupStrEqualNoCase(val, "LCLETTER"))
      pf.ListType(MarkerType::LowercaseEnglishLetter);
    else if (iupStrEqualNoCase(val, "UCLETTER"))
      pf.ListType(MarkerType::UppercaseEnglishLetter);
    else if (iupStrEqualNoCase(val, "LCROMAN"))
      pf.ListType(MarkerType::LowercaseRoman);
    else if (iupStrEqualNoCase(val, "UCROMAN"))
      pf.ListType(MarkerType::UppercaseRoman);
    else
      pf.ListType(MarkerType::None);

    char* style = iupAttribGet(formattag, "NUMBERINGSTYLE");
    if (style)
    {
      if (iupStrEqualNoCase(style, "RIGHTPARENTHESIS"))
        pf.ListStyle(MarkerStyle::Parenthesis);
      else if (iupStrEqualNoCase(style, "PARENTHESES"))
        pf.ListStyle(MarkerStyle::Parentheses);
      else if (iupStrEqualNoCase(style, "PERIOD"))
        pf.ListStyle(MarkerStyle::Period);
      else if (iupStrEqualNoCase(style, "NONUMBER"))
        pf.ListStyle(MarkerStyle::NoNumber);
      else
        pf.ListStyle(MarkerStyle::Plain);
    }

    char* numberingtab = iupAttribGet(formattag, "NUMBERINGTAB");
    if (numberingtab)
    {
      int tabval = 0;
      if (iupStrToInt(numberingtab, &tabval))
        pf.ListTab((float)tabval * pixelToPoint);
    }

    changed = true;
  }

  if (changed)
    range.ParagraphFormat(pf);
}

static int winuiTextHasRtfCharAttribs(Ihandle* formattag)
{
  return iupAttribGet(formattag, "RISE") != NULL;
}

static void winuiTextApplyCharFormatViaRtf(Ihandle* ih, Ihandle* formattag, ITextSelection const& sel)
{
  hstring plainText;
  sel.GetText(TextGetOptions::None, plainText);
  auto wtext = plainText.c_str();
  int textLen = (int)plainText.size();

  if (textLen <= 0)
    return;

  char* val;

  char defaultFontFace[128] = "";
  char* fontFace = iupAttribGet(formattag, "FONTFACE");
  if (fontFace)
  {
    const char* mapped_name = iupFontGetWinName(fontFace);
    if (mapped_name)
      fontFace = (char*)mapped_name;
  }
  else
  {
    char* face = iupGetFontFaceAttrib(ih);
    if (face)
    {
      strncpy(defaultFontFace, face, sizeof(defaultFontFace) - 1);
      defaultFontFace[sizeof(defaultFontFace) - 1] = '\0';
    }
  }

  unsigned char fgR = 0, fgG = 0, fgB = 0;
  int hasFg = 0;
  val = iupAttribGet(formattag, "FGCOLOR");
  if (val && iupStrToRGB(val, &fgR, &fgG, &fgB))
    hasFg = 1;

  unsigned char bgR = 0, bgG = 0, bgB = 0;
  int hasBg = 0;
  val = iupAttribGet(formattag, "BGCOLOR");
  if (val && iupStrToRGB(val, &bgR, &bgG, &bgB))
    hasBg = 1;

  size_t bufSize = 1024 + textLen * 12;
  char* rtf = (char*)malloc(bufSize);
  int pos = 0;

  pos += snprintf(rtf + pos, bufSize - pos, "{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0\\fnil %s;}}",
                 fontFace ? fontFace : defaultFontFace);

  if (hasFg || hasBg)
  {
    pos += snprintf(rtf + pos, bufSize - pos, "{\\colortbl;");
    if (hasFg)
      pos += snprintf(rtf + pos, bufSize - pos, "\\red%d\\green%d\\blue%d;", fgR, fgG, fgB);
    if (hasBg)
    {
      if (!hasFg)
        pos += snprintf(rtf + pos, bufSize - pos, ";");
      pos += snprintf(rtf + pos, bufSize - pos, "\\red%d\\green%d\\blue%d;", bgR, bgG, bgB);
    }
    pos += snprintf(rtf + pos, bufSize - pos, "}");
  }

  pos += snprintf(rtf + pos, bufSize - pos, "\\f0");

  val = iupAttribGet(formattag, "FONTSIZE");
  if (val)
  {
    int size = 0;
    if (iupStrToInt(val, &size))
    {
      float fontSize;
      if (size > 0)
        fontSize = (float)size;
      else
        fontSize = (float)(-size) * 72.0f / (float)iupdrvGetScreenDpi();

      char* fontscale = iupAttribGet(formattag, "FONTSCALE");
      if (fontscale)
      {
        double scale = 0;
        if (iupStrEqualNoCase(fontscale, "XX-SMALL")) scale = 0.5787;
        else if (iupStrEqualNoCase(fontscale, "X-SMALL")) scale = 0.6944;
        else if (iupStrEqualNoCase(fontscale, "SMALL")) scale = 0.8333;
        else if (iupStrEqualNoCase(fontscale, "MEDIUM")) scale = 1.0;
        else if (iupStrEqualNoCase(fontscale, "LARGE")) scale = 1.2;
        else if (iupStrEqualNoCase(fontscale, "X-LARGE")) scale = 1.44;
        else if (iupStrEqualNoCase(fontscale, "XX-LARGE")) scale = 1.728;
        else iupStrToDouble(fontscale, &scale);

        if (scale > 0)
          fontSize = (float)(fontSize * scale);
      }

      pos += snprintf(rtf + pos, bufSize - pos, "\\fs%d", (int)(fontSize * 2.0f));
    }
  }

  val = iupAttribGet(formattag, "ITALIC");
  if (val)
    pos += snprintf(rtf + pos, bufSize - pos, iupStrBoolean(val) ? "\\i" : "\\i0");

  val = iupAttribGet(formattag, "STRIKEOUT");
  if (val)
    pos += snprintf(rtf + pos, bufSize - pos, iupStrBoolean(val) ? "\\strike" : "\\strike0");

  val = iupAttribGet(formattag, "UNDERLINE");
  if (val)
  {
    if (iupStrEqualNoCase(val, "SINGLE"))
      pos += snprintf(rtf + pos, bufSize - pos, "\\ul");
    else if (iupStrEqualNoCase(val, "DOUBLE"))
      pos += snprintf(rtf + pos, bufSize - pos, "\\uldb");
    else if (iupStrEqualNoCase(val, "DOTTED"))
      pos += snprintf(rtf + pos, bufSize - pos, "\\uld");
    else
      pos += snprintf(rtf + pos, bufSize - pos, "\\ulnone");
  }

  val = iupAttribGet(formattag, "WEIGHT");
  if (val)
  {
    if (iupStrEqualNoCase(val, "BOLD") || iupStrEqualNoCase(val, "EXTRABOLD") ||
        iupStrEqualNoCase(val, "HEAVY") || iupStrEqualNoCase(val, "SEMIBOLD"))
      pos += snprintf(rtf + pos, bufSize - pos, "\\b");
    else
      pos += snprintf(rtf + pos, bufSize - pos, "\\b0");
  }

  val = iupAttribGet(formattag, "RISE");
  if (val)
  {
    if (iupStrEqualNoCase(val, "SUPERSCRIPT"))
      pos += snprintf(rtf + pos, bufSize - pos, "\\super");
    else if (iupStrEqualNoCase(val, "SUBSCRIPT"))
      pos += snprintf(rtf + pos, bufSize - pos, "\\sub");
    else
      pos += snprintf(rtf + pos, bufSize - pos, "\\nosupersub");
  }

  if (hasFg)
    pos += snprintf(rtf + pos, bufSize - pos, "\\cf1");
  if (hasBg)
    pos += snprintf(rtf + pos, bufSize - pos, "\\highlight%d", hasFg ? 2 : 1);

  val = iupAttribGet(formattag, "SMALLCAPS");
  if (val)
    pos += snprintf(rtf + pos, bufSize - pos, iupStrBoolean(val) ? "\\scaps" : "\\scaps0");

  val = iupAttribGet(formattag, "PROTECTED");
  if (val)
    pos += snprintf(rtf + pos, bufSize - pos, iupStrBoolean(val) ? "\\protect" : "\\protect0");

  rtf[pos++] = ' ';

  for (int i = 0; i < textLen; i++)
  {
    wchar_t ch = wtext[i];
    if (ch == L'\\')
    { rtf[pos++] = '\\'; rtf[pos++] = '\\'; }
    else if (ch == L'{')
    { rtf[pos++] = '\\'; rtf[pos++] = '{'; }
    else if (ch == L'}')
    { rtf[pos++] = '\\'; rtf[pos++] = '}'; }
    else if (ch == L'\r')
    {
      memcpy(rtf + pos, "\\par ", 5);
      pos += 5;
    }
    else if (ch < 128)
      rtf[pos++] = (char)ch;
    else
      pos += snprintf(rtf + pos, bufSize - pos, "\\u%d?", (int)(int16_t)ch);
  }

  rtf[pos++] = '}';
  rtf[pos] = '\0';

  sel.SetText(TextSetOptions::FormatRtf, winrt::to_hstring(rtf));
  free(rtf);
}

extern "C" IUP_SDK_API void iupdrvTextAddFormatTag(Ihandle* ih, Ihandle* formattag, int bulk)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !aux->isFormatted)
    return;

  RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
  if (!reb)
    return;

  bool wasReadOnly = reb.IsReadOnly();
  if (wasReadOnly)
    reb.IsReadOnly(false);

  (void)bulk;

  if (iupAttribGet(formattag, "FONTSCALE") && !iupAttribGet(formattag, "FONTSIZE"))
    iupAttribSet(formattag, "FONTSIZE", iupGetFontSizeAttrib(ih));

  auto doc = reb.Document();
  auto sel = doc.Selection();

  int save_start = sel.StartPosition();
  int save_end = sel.EndPosition();

  int start_pos = -1, end_pos = -1;

  char* selection = iupAttribGet(formattag, "SELECTION");
  if (selection)
  {
    if (iupStrEqualNoCase(selection, "ALL"))
    {
      hstring text;
      doc.GetText(TextGetOptions::None, text);
      start_pos = 0;
      end_pos = (int)text.size();
    }
    else if (!iupStrEqualNoCase(selection, "NONE"))
    {
      int lin_start = 1, col_start = 1, lin_end = 1, col_end = 1;
      if (sscanf(selection, "%d,%d:%d,%d", &lin_start, &col_start, &lin_end, &col_end) == 4)
      {
        start_pos = winuiTextLinColToPos(doc, lin_start, col_start);
        end_pos = winuiTextLinColToPos(doc, lin_end, col_end);
      }
    }
  }
  else
  {
    char* selectionpos = iupAttribGet(formattag, "SELECTIONPOS");
    if (selectionpos)
    {
      if (iupStrEqualNoCase(selectionpos, "ALL"))
      {
        hstring text;
        doc.GetText(TextGetOptions::None, text);
        start_pos = 0;
        end_pos = (int)text.size();
      }
      else if (!iupStrEqualNoCase(selectionpos, "NONE"))
      {
        iupStrToIntInt(selectionpos, &start_pos, &end_pos, ':');
      }
    }
  }

  if (start_pos >= 0 && end_pos >= 0)
    sel.SetRange(start_pos, end_pos);

  {
    char* image_name = iupAttribGet(formattag, "IMAGE");
    if (image_name)
    {
      void* handle = iupImageGetImage(image_name, ih, 0, NULL);
      WriteableBitmap bitmap = winuiGetBitmapFromHandle(handle);

      if (bitmap)
      {
        int img_w, img_h;
        int new_w = 0, new_h = 0;
        char* attr;

        iupImageGetInfo(image_name, &img_w, &img_h, NULL);

        attr = iupAttribGet(formattag, "WIDTH");
        if (attr) iupStrToInt(attr, &new_w);
        attr = iupAttribGet(formattag, "HEIGHT");
        if (attr) iupStrToInt(attr, &new_h);

        if (new_w <= 0) new_w = img_w;
        if (new_h <= 0) new_h = img_h;

        int width = bitmap.PixelWidth();
        int height = bitmap.PixelHeight();
        IBuffer pixelBuffer = bitmap.PixelBuffer();
        uint8_t* pixels = pixelBuffer.data();
        uint32_t pixelDataSize = width * height * 4;

        InMemoryRandomAccessStream pngStream;
        auto encoder = Windows::Graphics::Imaging::BitmapEncoder::CreateAsync(
            Windows::Graphics::Imaging::BitmapEncoder::PngEncoderId(), pngStream).get();
        encoder.SetPixelData(
            Windows::Graphics::Imaging::BitmapPixelFormat::Bgra8,
            Windows::Graphics::Imaging::BitmapAlphaMode::Premultiplied,
            width, height, 96.0, 96.0,
            winrt::array_view<uint8_t const>(pixels, pixels + pixelDataSize));
        if (new_w != width || new_h != height)
        {
          auto transform = encoder.BitmapTransform();
          transform.ScaledWidth(new_w);
          transform.ScaledHeight(new_h);
        }
        encoder.FlushAsync().get();

        uint32_t pngSize = (uint32_t)pngStream.Size();
        pngStream.Seek(0);
        Buffer pngBuf(pngSize);
        pngStream.ReadAsync(pngBuf, pngSize, InputStreamOptions::None).get();
        uint8_t* pngData = pngBuf.data();

        double dpi = iupdrvGetScreenDpi();
        int twips_w = (int)(new_w * 1440.0 / dpi);
        int twips_h = (int)(new_h * 1440.0 / dpi);

        size_t rtfBufSize = 256 + pngSize * 2;
        char* rtf = (char*)malloc(rtfBufSize);
        int pos = snprintf(rtf, rtfBufSize, "{\\rtf1{\\pict\\pngblip\\picw%d\\pich%d\\picwgoal%d\\pichgoal%d ",
                          new_w, new_h, twips_w, twips_h);

        static constexpr char hexChars[] = "0123456789abcdef";
        for (uint32_t i = 0; i < pngSize; i++)
        {
          rtf[pos++] = hexChars[pngData[i] >> 4];
          rtf[pos++] = hexChars[pngData[i] & 0x0f];
        }
        rtf[pos++] = '}';
        rtf[pos++] = '}';
        rtf[pos] = '\0';

        sel.SetText(TextSetOptions::FormatRtf, winrt::to_hstring(rtf));
        free(rtf);
      }

      sel.SetRange(save_start, save_end);
      return;
    }
  }

  {
    int range_start = sel.StartPosition();
    int range_end = sel.EndPosition();
    ITextRange selRange = sel.as<ITextRange>();

    if (winuiTextHasRtfCharAttribs(formattag))
    {
      winuiTextApplyCharFormatViaRtf(ih, formattag, sel);
      sel.SetRange(range_start, range_end);

      if (iupAttribGet(formattag, "LINK"))
        winuiTextParseCharacterFormat(formattag, selRange);
    }
    else
    {
      winuiTextParseCharacterFormat(formattag, selRange);
    }

    winuiTextParseParagraphFormat(formattag, selRange);

    {
      char* link_url = iupAttribGet(formattag, "LINK");
      if (link_url)
      {
        int idx = iupAttribGetInt(ih, "_IUP_LINK_COUNT");
        char attr_name[80];

        snprintf(attr_name, sizeof(attr_name), "_IUP_LINK_URL_%d", idx);
        iupAttribSetStr(ih, attr_name, link_url);

        snprintf(attr_name, sizeof(attr_name), "_IUP_LINK_RANGE_%d", idx);
        iupAttribSetStrf(ih, attr_name, "%d:%d", range_start, range_end);

        iupAttribSetInt(ih, "_IUP_LINK_COUNT", idx + 1);
      }
    }
  }

  sel.SetRange(save_start, save_end);

  if (wasReadOnly)
    reb.IsReadOnly(true);
}

static const char* winuiTextFindLinkUrl(Ihandle* ih, int pos)
{
  int count = iupAttribGetInt(ih, "_IUP_LINK_COUNT");

  for (int i = 0; i < count; i++)
  {
    int start, end;
    char attr_name[80];

    snprintf(attr_name, sizeof(attr_name), "_IUP_LINK_RANGE_%d", i);
    if (iupStrToIntInt(iupAttribGet(ih, attr_name), &start, &end, ':') == 2)
    {
      if (pos >= start && pos < end)
      {
        snprintf(attr_name, sizeof(attr_name), "_IUP_LINK_URL_%d", i);
        return iupAttribGet(ih, attr_name);
      }
    }
  }

  return NULL;
}

static int winui_singleline_extra_h = -1;

static void winuiTextMeasureSingleLineMetrics(void)
{
  if (winui_singleline_extra_h >= 0)
    return;

  TextBox tb;
  tb.BorderThickness(Thickness{0, 0, 0, 0});
  tb.Padding(Thickness{0, 0, 0, 0});
  tb.Text(L"Wj");
  tb.Measure(Size(10000, 10000));
  int textbox_height = (int)ceil(tb.DesiredSize().Height);

  TextBlock tblock;
  tblock.Text(L"Wj");
  tblock.Measure(Size(10000, 10000));
  int text_height = (int)ceil(tblock.DesiredSize().Height);

  winui_singleline_extra_h = textbox_height - text_height;
  if (winui_singleline_extra_h < 0) winui_singleline_extra_h = 0;
}

extern "C" IUP_SDK_API void iupdrvTextAddExtraPadding(Ihandle* ih, int* x, int* y)
{
  (void)ih;
  winuiTextMeasureSingleLineMetrics();
  *x += winui_singleline_extra_h;
  *y += winui_singleline_extra_h;
}

static char* winuiTextGetSelectedTextAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return NULL;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      auto sel = reb.Document().Selection();
      if (sel.StartPosition() != sel.EndPosition())
      {
        hstring text;
        sel.GetText(TextGetOptions::None, text);
        return iupwinuiHStringToString(text);
      }
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
    {
      int start = tb.SelectionStart();
      int len = tb.SelectionLength();
      if (len > 0)
      {
        hstring fullText = tb.Text();
        std::wstring selected(fullText.c_str() + start, len);
        return iupwinuiHStringToString(hstring(selected));
      }
    }
  }

  return NULL;
}

static int winuiTextSetSelectedTextAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;

  if (!value)
    value = "";

  ih->data->disable_callbacks = 1;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      auto sel = reb.Document().Selection();
      if (sel.StartPosition() != sel.EndPosition())
        sel.SetText(TextSetOptions::None, iupwinuiStringToHString(value));
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
    {
      int start = tb.SelectionStart();
      int len = tb.SelectionLength();
      if (len > 0)
      {
        std::wstring fullText(tb.Text().c_str());
        std::wstring replacement = iupwinuiStringToWString(value);
        fullText.replace(start, len, replacement);
        iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
        tb.Text(hstring(fullText));
        tb.Select(start + (int)replacement.size(), 0);
        aux->savedText = fullText;
      }
    }
  }

  ih->data->disable_callbacks = 0;
  return 0;
}

static char* winuiTextGetSelectionAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return NULL;

  int start, end;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (!reb) return NULL;
    auto sel = reb.Document().Selection();
    start = sel.StartPosition();
    end = sel.EndPosition();
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (!tb) return NULL;
    start = tb.SelectionStart();
    end = start + tb.SelectionLength();
  }

  if (start == end)
    return NULL;

  if (ih->data->is_multiline)
  {
    int lin1, col1, lin2, col2;
    iupdrvTextConvertPosToLinCol(ih, start, &lin1, &col1);
    iupdrvTextConvertPosToLinCol(ih, end, &lin2, &col2);
    return iupStrReturnStrf("%d,%d:%d,%d", lin1, col1, lin2, col2);
  }
  else
    return iupStrReturnIntInt(start + 1, end + 1, ':');
}

static int winuiTextSetSelectionAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;
  if (!value)
    return 0;

  if (iupStrEqualNoCase(value, "NONE"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
      {
        auto sel = reb.Document().Selection();
        sel.SetRange(sel.StartPosition(), sel.StartPosition());
      }
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
        tb.Select(tb.SelectionStart(), 0);
    }
    return 0;
  }

  if (iupStrEqualNoCase(value, "ALL"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
      {
        hstring text;
        reb.Document().GetText(TextGetOptions::None, text);
        reb.Document().Selection().SetRange(0, (int32_t)text.size());
      }
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
        tb.SelectAll();
    }
    return 0;
  }

  if (ih->data->is_multiline)
  {
    int lin1 = 1, col1 = 1, lin2 = 1, col2 = 1;
    if (sscanf(value, "%d,%d:%d,%d", &lin1, &col1, &lin2, &col2) != 4)
      return 0;

    int start, end;
    iupdrvTextConvertLinColToPos(ih, lin1, col1, &start);
    iupdrvTextConvertLinColToPos(ih, lin2, col2, &end);

    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
        reb.Document().Selection().SetRange(start, end);
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
        tb.Select(start, end - start);
    }
  }
  else
  {
    int start = 1, end = 1;
    if (iupStrToIntInt(value, &start, &end, ':') != 2)
      return 0;
    start--;
    end--;
    if (start < 0) start = 0;
    if (end < 0) end = 0;

    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Select(start, end - start);
  }

  return 0;
}

static char* winuiTextGetSelectionPosAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return NULL;

  int start, end;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (!reb) return NULL;
    auto sel = reb.Document().Selection();
    start = sel.StartPosition();
    end = sel.EndPosition();
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (!tb) return NULL;
    start = tb.SelectionStart();
    end = start + tb.SelectionLength();
  }

  if (start == end)
    return NULL;

  return iupStrReturnIntInt(start, end, ':');
}

static int winuiTextSetSelectionPosAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;
  if (!value)
    return 0;

  if (iupStrEqualNoCase(value, "NONE"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
      {
        auto sel = reb.Document().Selection();
        sel.SetRange(sel.StartPosition(), sel.StartPosition());
      }
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
        tb.Select(tb.SelectionStart(), 0);
    }
    return 0;
  }

  if (iupStrEqualNoCase(value, "ALL"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
      {
        hstring text;
        reb.Document().GetText(TextGetOptions::None, text);
        reb.Document().Selection().SetRange(0, (int32_t)text.size());
      }
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
        tb.SelectAll();
    }
    return 0;
  }

  int start = 0, end = 0;
  if (iupStrToIntInt(value, &start, &end, ':') != 2)
    return 0;
  if (start < 0) start = 0;
  if (end < 0) end = 0;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.Document().Selection().SetRange(start, end);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Select(start, end - start);
  }

  return 0;
}

static char* winuiTextGetCaretAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return NULL;

  int pos;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (!reb) return NULL;
    pos = reb.Document().Selection().StartPosition();
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (!tb) return NULL;
    pos = tb.SelectionStart();
  }

  if (ih->data->is_multiline)
  {
    int lin, col;
    iupdrvTextConvertPosToLinCol(ih, pos, &lin, &col);
    return iupStrReturnIntInt(lin, col, ',');
  }
  else
    return iupStrReturnIntInt(1, pos + 1, ',');
}

static int winuiTextSetCaretAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;
  if (!value)
    return 0;

  int lin = 1, col = 1;
  iupStrToIntInt(value, &lin, &col, ',');
  if (lin < 1) lin = 1;
  if (col < 1) col = 1;

  int pos;
  if (ih->data->is_multiline)
    iupdrvTextConvertLinColToPos(ih, lin, col, &pos);
  else
    pos = col - 1;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.Document().Selection().SetRange(pos, pos);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Select(pos, 0);
  }

  return 0;
}

static char* winuiTextGetCaretPosAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return NULL;

  int pos;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (!reb) return NULL;
    pos = reb.Document().Selection().StartPosition();
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (!tb) return NULL;
    pos = tb.SelectionStart();
  }

  return iupStrReturnInt(pos);
}

static int winuiTextSetCaretPosAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;
  if (!value)
    return 0;

  int pos = 0;
  iupStrToInt(value, &pos);
  if (pos < 0) pos = 0;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.Document().Selection().SetRange(pos, pos);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Select(pos, 0);
  }

  return 0;
}

static int winuiTextSetInsertAttrib(Ihandle* ih, const char* value)
{
  if (!ih->handle || !value)
    return 0;

  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;

  ih->data->disable_callbacks = 1;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.Document().Selection().SetText(TextSetOptions::None, iupwinuiStringToHString(value));
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
    {
      int pos = tb.SelectionStart();
      int len = tb.SelectionLength();
      std::wstring fullText(tb.Text().c_str());
      std::wstring insertText = iupwinuiStringToWString(value);
      fullText.replace(pos, len, insertText);
      iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
      tb.Text(hstring(fullText));
      tb.Select(pos + (int)insertText.size(), 0);
      aux->savedText = fullText;
    }
  }

  ih->data->disable_callbacks = 0;
  return 0;
}

static char* winuiTextGetCountAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return NULL;

  int count;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (!reb) return NULL;
    hstring text;
    reb.Document().GetText(TextGetOptions::None, text);
    count = (int)text.size();
    if (count > 0 && text.c_str()[count - 1] == L'\r')
      count--;
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (!tb) return NULL;
    count = (int)tb.Text().size();
  }

  return iupStrReturnInt(count);
}

static char* winuiTextGetLineCountAttrib(Ihandle* ih)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
    return NULL;

  if (!ih->data->is_multiline)
    return iupStrReturnInt(1);

  int lineCount = 1;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (!reb) return NULL;
    hstring text;
    reb.Document().GetText(TextGetOptions::None, text);
    int len = (int)text.size();
    if (len > 0 && text.c_str()[len - 1] == L'\r')
      len--;
    for (int i = 0; i < len; i++)
    {
      if (text.c_str()[i] == L'\r')
        lineCount++;
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (!tb) return NULL;
    hstring text = tb.Text();
    for (uint32_t i = 0; i < text.size(); i++)
    {
      if (text.c_str()[i] == L'\r')
        lineCount++;
    }
  }

  return iupStrReturnInt(lineCount);
}

static int winuiTextSetNCAttrib(Ihandle* ih, const char* value)
{
  int nc = 0;
  if (value)
    iupStrToInt(value, &nc);
  ih->data->nc = nc;

  if (!ih->handle)
    return 1;

  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isFormatted)
    return 0;

  if (!ih->data->is_multiline)
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.MaxLength(nc);
  }

  return 0;
}

static void winuiTextCopyToClipboard(const wchar_t* text)
{
  if (!OpenClipboard(NULL))
    return;
  EmptyClipboard();
  size_t len = wcslen(text) + 1;
  HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t));
  if (hMem)
  {
    wchar_t* pMem = (wchar_t*)GlobalLock(hMem);
    if (pMem)
    {
      memcpy(pMem, text, len * sizeof(wchar_t));
      GlobalUnlock(hMem);
      SetClipboardData(CF_UNICODETEXT, hMem);
    }
  }
  CloseClipboard();
}

static wchar_t* winuiTextGetFromClipboard(void)
{
  if (!OpenClipboard(NULL))
    return NULL;
  HANDLE hData = GetClipboardData(CF_UNICODETEXT);
  wchar_t* result = NULL;
  if (hData)
  {
    wchar_t* pszText = (wchar_t*)GlobalLock(hData);
    if (pszText)
    {
      size_t len = wcslen(pszText) + 1;
      result = (wchar_t*)malloc(len * sizeof(wchar_t));
      if (result)
        memcpy(result, pszText, len * sizeof(wchar_t));
      GlobalUnlock(hData);
    }
  }
  CloseClipboard();
  return result;
}

static int winuiTextSetClipboardAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;

  if (iupStrEqualNoCase(value, "COPY"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
        reb.Document().Selection().Copy();
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
      {
        int start = tb.SelectionStart();
        int len = tb.SelectionLength();
        if (len > 0)
        {
          hstring text = tb.Text();
          std::wstring selected(text.c_str() + start, len);
          winuiTextCopyToClipboard(selected.c_str());
        }
      }
    }
  }
  else if (iupStrEqualNoCase(value, "CUT"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
        reb.Document().Selection().Cut();
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
      {
        int start = tb.SelectionStart();
        int len = tb.SelectionLength();
        if (len > 0)
        {
          hstring text = tb.Text();
          std::wstring selected(text.c_str() + start, len);
          winuiTextCopyToClipboard(selected.c_str());
          ih->data->disable_callbacks = 1;
          iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
          std::wstring fullText(text.c_str());
          fullText.erase(start, len);
          tb.Text(hstring(fullText));
          tb.Select(start, 0);
          aux->savedText = fullText;
          ih->data->disable_callbacks = 0;
        }
      }
    }
  }
  else if (iupStrEqualNoCase(value, "PASTE"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
        reb.Document().Selection().Paste(0);
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
      {
        wchar_t* pasteText = winuiTextGetFromClipboard();
        if (pasteText)
        {
          int start = tb.SelectionStart();
          int len = tb.SelectionLength();
          std::wstring fullText(tb.Text().c_str());
          std::wstring paste(pasteText);
          ih->data->disable_callbacks = 1;
          iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
          fullText.replace(start, len, paste);
          tb.Text(hstring(fullText));
          tb.Select(start + (int)paste.size(), 0);
          aux->savedText = fullText;
          ih->data->disable_callbacks = 0;
          free(pasteText);
        }
      }
    }
  }
  else if (iupStrEqualNoCase(value, "CLEAR"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
        reb.Document().Selection().SetText(TextSetOptions::None, L"");
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
      {
        int start = tb.SelectionStart();
        int len = tb.SelectionLength();
        if (len > 0)
        {
          ih->data->disable_callbacks = 1;
          iupAttribSet(ih, "_IUPWINUI_IGNORE_VALUECHANGED", "1");
          std::wstring fullText(tb.Text().c_str());
          fullText.erase(start, len);
          tb.Text(hstring(fullText));
          tb.Select(start, 0);
          aux->savedText = fullText;
          ih->data->disable_callbacks = 0;
        }
      }
    }
  }
  else if (iupStrEqualNoCase(value, "UNDO"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
        reb.Document().Undo();
    }
  }
  else if (iupStrEqualNoCase(value, "REDO"))
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
        reb.Document().Redo();
    }
  }

  return 0;
}

static int winuiTextSetScrollToAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;
  if (!value)
    return 0;

  if (ih->data->is_multiline)
  {
    int lin = 1, col = 1;
    iupStrToIntInt(value, &lin, &col, ',');
    if (lin < 1) lin = 1;
    if (col < 1) col = 1;

    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
      {
        int pos = winuiTextLinColToPos(reb.Document(), lin, col);
        auto range = reb.Document().GetRange(pos, pos);
        range.ScrollIntoView(PointOptions::Start);
      }
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
      {
        ScrollViewer sv = winuiTextFindScrollViewer(tb);
        if (sv)
        {
          float lineHeight = iupwinuiFontGetMultilineLineHeightF(ih);
          double yOffset = (lin - 1) * lineHeight;
          sv.ChangeView(nullptr, yOffset, nullptr);
        }
      }
    }
  }
  else
  {
    int pos = 1;
    iupStrToInt(value, &pos);
    if (pos < 1) pos = 1;
    pos--;

    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Select(pos, 0);
  }

  return 0;
}

static int winuiTextSetScrollToPosAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;
  if (!value)
    return 0;

  int pos = 0;
  iupStrToInt(value, &pos);
  if (pos < 0) pos = 0;

  if (ih->data->is_multiline)
  {
    if (aux->isFormatted)
    {
      RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
      if (reb)
      {
        auto range = reb.Document().GetRange(pos, pos);
        range.ScrollIntoView(PointOptions::Start);
      }
    }
    else
    {
      TextBox tb = winuiGetHandle<TextBox>(ih);
      if (tb)
      {
        int lin, col;
        hstring text = tb.Text();
        winuiTextStringPosToLinCol(text.c_str(), (int)text.size(), pos, &lin, &col);
        ScrollViewer sv = winuiTextFindScrollViewer(tb);
        if (sv)
        {
          float lineHeight = iupwinuiFontGetMultilineLineHeightF(ih);
          double yOffset = (lin - 1) * lineHeight;
          sv.ChangeView(nullptr, yOffset, nullptr);
        }
      }
    }
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Select(pos, 0);
  }

  return 0;
}

static int winuiTextSetAlignmentAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;

  TextAlignment align = TextAlignment::Left;
  if (iupStrEqualNoCase(value, "ARIGHT"))
    align = TextAlignment::Right;
  else if (iupStrEqualNoCase(value, "ACENTER"))
    align = TextAlignment::Center;

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.TextAlignment(align);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.TextAlignment(align);
  }

  return 1;
}

static int winuiTextSetBgColorAttrib(Ihandle* ih, const char* value)
{
  unsigned char r, g, b;
  if (!iupStrToRGB(value, &r, &g, &b))
    return 0;

  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
    return 0;

  Windows::UI::Color color;
  color.A = 255; color.R = r; color.G = g; color.B = b;
  SolidColorBrush brush(color);

  if (aux->isSpin)
  {
    NumberBox nb = winuiGetHandle<NumberBox>(ih);
    if (nb)
      nb.Background(brush);
  }
  else if (aux->isPassword)
  {
    PasswordBox pb = winuiGetHandle<PasswordBox>(ih);
    if (pb)
      pb.Background(brush);
  }
  else if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.Background(brush);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Background(brush);
  }

  return 1;
}

static int winuiTextSetFgColorAttrib(Ihandle* ih, const char* value)
{
  unsigned char r, g, b;
  if (!iupStrToRGB(value, &r, &g, &b))
    return 0;

  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux)
    return 0;

  Windows::UI::Color color;
  color.A = 255; color.R = r; color.G = g; color.B = b;
  SolidColorBrush brush(color);

  if (aux->isSpin)
  {
    NumberBox nb = winuiGetHandle<NumberBox>(ih);
    if (nb)
      nb.Foreground(brush);
  }
  else if (aux->isPassword)
  {
    PasswordBox pb = winuiGetHandle<PasswordBox>(ih);
    if (pb)
      pb.Foreground(brush);
  }
  else if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.Foreground(brush);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Foreground(brush);
  }

  return 1;
}

static int winuiTextSetPaddingAttrib(Ihandle* ih, const char* value)
{
  iupStrToIntInt(value, &(ih->data->horiz_padding), &(ih->data->vert_padding), 'x');

  if (!ih->handle)
    return 1;

  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;

  Thickness padding = {(double)ih->data->horiz_padding, (double)ih->data->vert_padding,
                       (double)ih->data->horiz_padding, (double)ih->data->vert_padding};

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
      reb.Padding(padding);
  }
  else
  {
    TextBox tb = winuiGetHandle<TextBox>(ih);
    if (tb)
      tb.Padding(padding);
  }

  return 0;
}

static int winuiTextSetCueBannerAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || aux->isSpin || aux->isPassword)
    return 0;

  if (ih->data->is_multiline)
    return 0;

  TextBox tb = winuiGetHandle<TextBox>(ih);
  if (tb)
    tb.PlaceholderText(iupwinuiStringToHString(value ? value : ""));

  return 1;
}

static int winuiTextSetTabSizeAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !ih->data->is_multiline)
    return 0;

  int tabsize = 8;
  iupStrToInt(value, &tabsize);

  if (aux->isFormatted)
  {
    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      int charwidth;
      iupdrvFontGetCharSize(ih, &charwidth, NULL);
      float tabStopPoints = (float)(tabsize * charwidth) * 72.0f / (float)iupdrvGetScreenDpi();
      reb.Document().DefaultTabStop(tabStopPoints);
    }
  }

  return 1;
}

static int winuiTextSetRemoveFormattingAttrib(Ihandle* ih, const char* value)
{
  IupWinUITextAux* aux = winuiGetAux<IupWinUITextAux>(ih, IUPWINUI_TEXT_AUX);
  if (!aux || !aux->isFormatted)
    return 0;

  RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
  if (!reb)
    return 0;

  bool wasReadOnly = reb.IsReadOnly();
  if (wasReadOnly)
    reb.IsReadOnly(false);

  auto doc = reb.Document();
  auto defaultCF = doc.GetDefaultCharacterFormat();
  auto defaultPF = doc.GetDefaultParagraphFormat();

  if (iupStrEqualNoCase(value, "ALL"))
  {
    hstring text;
    doc.GetText(TextGetOptions::None, text);
    auto range = doc.GetRange(0, (int32_t)text.size());
    range.CharacterFormat().SetClone(defaultCF);
    range.ParagraphFormat().SetClone(defaultPF);
  }
  else
  {
    auto sel = doc.Selection();
    if (sel.StartPosition() != sel.EndPosition())
    {
      auto range = sel.GetClone();
      range.CharacterFormat().SetClone(defaultCF);
      range.ParagraphFormat().SetClone(defaultPF);
    }
  }

  if (wasReadOnly)
    reb.IsReadOnly(true);

  return 0;
}

static int winuiTextSetFontAttrib(Ihandle* ih, const char* value)
{
  if (ih->data->has_formatting && ih->handle)
  {
    if (!iupAttribGet(ih, "_IUPWINUI_FONTUPDATECHECK"))
    {
      char* cur_value = iupAttribGetStr(ih, "FONT");
      if (iupStrEqual(value, cur_value))
        return 0;
    }
    iupAttribSet(ih, "_IUPWINUI_FONTUPDATECHECK", NULL);

    int ret = iupdrvSetFontAttrib(ih, value);

    RichEditBox reb = winuiGetHandle<RichEditBox>(ih);
    if (reb)
    {
      reb.ClearValue(Control::FontSizeProperty());
      reb.ClearValue(Control::FontFamilyProperty());
    }

    return ret;
  }

  return iupdrvSetFontAttrib(ih, value);
}

static int winuiTextSetMaskFloatAttrib(Ihandle* ih, const char* value)
{
  if (!value)
  {
    if (ih->data->mask)
    {
      iupMaskDestroy(ih->data->mask);
      ih->data->mask = NULL;
    }
  }
  else
  {
    Imask* mask;
    float min, max;
    char* decimal_symbol = iupAttribGet(ih, "MASKDECIMALSYMBOL");
    if (!decimal_symbol)
      decimal_symbol = IupGetGlobal("DEFAULTDECIMALSYMBOL");

    if (iupStrToFloatFloat(value, &min, &max, ':') != 2)
      return 0;

    iupAttribSetDouble(ih, "_IUPWINUI_MASKFLOAT_MIN", (double)min);
    iupAttribSetDouble(ih, "_IUPWINUI_MASKFLOAT_MAX", (double)max);

    mask = iupMaskCreateFloat(min, max, decimal_symbol);
    if (mask)
    {
      int val = iupAttribGetInt(ih, "MASKNOEMPTY");
      iupMaskSetNoEmpty(mask, val);

      if (ih->data->mask)
        iupMaskDestroy(ih->data->mask);

      ih->data->mask = mask;
    }
  }
  return 0;
}

extern "C" IUP_SDK_API void iupdrvTextInitClass(Iclass* ic)
{
  ic->Map = winuiTextMapMethod;
  ic->UnMap = winuiTextUnMapMethod;

  iupClassRegisterAttribute(ic, "FONT", NULL, winuiTextSetFontAttrib, IUPAF_SAMEASSYSTEM, "DEFAULTFONT", IUPAF_NOT_MAPPED);

  iupClassRegisterAttribute(ic, "VALUE", winuiTextGetValueAttrib, winuiTextSetValueAttrib, NULL, NULL, IUPAF_NO_DEFAULTVALUE|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "APPEND", NULL, winuiTextSetAppendAttrib, NULL, NULL, IUPAF_NOT_MAPPED|IUPAF_WRITEONLY|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "READONLY", winuiTextGetReadOnlyAttrib, winuiTextSetReadOnlyAttrib, NULL, NULL, IUPAF_DEFAULT);
  iupClassRegisterAttribute(ic, "LINEVALUE", winuiTextGetLineValueAttrib, NULL, NULL, NULL, IUPAF_READONLY|IUPAF_NO_DEFAULTVALUE|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "ACTIVE", NULL, winuiTextSetActiveAttrib, IUPAF_SAMEASSYSTEM, "YES", IUPAF_DEFAULT);

  iupClassRegisterAttribute(ic, "SPINMIN", NULL, winuiTextSetSpinMinAttrib, IUPAF_SAMEASSYSTEM, "0", IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "SPINMAX", NULL, winuiTextSetSpinMaxAttrib, IUPAF_SAMEASSYSTEM, "100", IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "SPININC", NULL, winuiTextSetSpinIncAttrib, IUPAF_SAMEASSYSTEM, "1", IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "SPINVALUE", winuiTextGetSpinValueAttrib, winuiTextSetSpinValueAttrib, IUPAF_SAMEASSYSTEM, "0", IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "MASKFLOAT", NULL, winuiTextSetMaskFloatAttrib, NULL, NULL, IUPAF_NOT_MAPPED|IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "FORMATTING", iupTextGetFormattingAttrib, iupTextSetFormattingAttrib, NULL, NULL, IUPAF_NOT_MAPPED|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "ADDFORMATTAG", NULL, iupTextSetAddFormatTagAttrib, NULL, NULL, IUPAF_IHANDLENAME|IUPAF_NOT_MAPPED|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "ADDFORMATTAG_HANDLE", NULL, iupTextSetAddFormatTagHandleAttrib, NULL, NULL, IUPAF_IHANDLE|IUPAF_NOT_MAPPED|IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "SELECTEDTEXT", winuiTextGetSelectedTextAttrib, winuiTextSetSelectedTextAttrib, NULL, NULL, IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "SELECTION", winuiTextGetSelectionAttrib, winuiTextSetSelectionAttrib, NULL, NULL, IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "SELECTIONPOS", winuiTextGetSelectionPosAttrib, winuiTextSetSelectionPosAttrib, NULL, NULL, IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "CARET", winuiTextGetCaretAttrib, winuiTextSetCaretAttrib, NULL, NULL, IUPAF_NO_SAVE|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "CARETPOS", winuiTextGetCaretPosAttrib, winuiTextSetCaretPosAttrib, IUPAF_SAMEASSYSTEM, "0", IUPAF_NO_SAVE|IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "INSERT", NULL, winuiTextSetInsertAttrib, NULL, NULL, IUPAF_NOT_MAPPED|IUPAF_WRITEONLY|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "CLIPBOARD", NULL, winuiTextSetClipboardAttrib, NULL, NULL, IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "COUNT", winuiTextGetCountAttrib, NULL, NULL, NULL, IUPAF_READONLY|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "LINECOUNT", winuiTextGetLineCountAttrib, NULL, NULL, NULL, IUPAF_READONLY|IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "NC", iupTextGetNCAttrib, winuiTextSetNCAttrib, IUPAF_SAMEASSYSTEM, "0", IUPAF_NOT_MAPPED);

  iupClassRegisterAttribute(ic, "SCROLLTO", NULL, winuiTextSetScrollToAttrib, NULL, NULL, IUPAF_WRITEONLY|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "SCROLLTOPOS", NULL, winuiTextSetScrollToPosAttrib, NULL, NULL, IUPAF_WRITEONLY|IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "ALIGNMENT", NULL, winuiTextSetAlignmentAttrib, IUPAF_SAMEASSYSTEM, "ALEFT", IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "BGCOLOR", NULL, winuiTextSetBgColorAttrib, IUPAF_SAMEASSYSTEM, "TXTBGCOLOR", IUPAF_DEFAULT);
  iupClassRegisterAttribute(ic, "FGCOLOR", NULL, winuiTextSetFgColorAttrib, IUPAF_SAMEASSYSTEM, "TXTFGCOLOR", IUPAF_NOT_MAPPED);
  iupClassRegisterAttribute(ic, "PADDING", iupTextGetPaddingAttrib, winuiTextSetPaddingAttrib, IUPAF_SAMEASSYSTEM, "0x0", IUPAF_NOT_MAPPED);
  iupClassRegisterAttribute(ic, "CUEBANNER", NULL, winuiTextSetCueBannerAttrib, NULL, NULL, IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "TABSIZE", NULL, winuiTextSetTabSizeAttrib, IUPAF_SAMEASSYSTEM, "8", IUPAF_DEFAULT);
  iupClassRegisterAttribute(ic, "REMOVEFORMATTING", NULL, winuiTextSetRemoveFormattingAttrib, NULL, NULL, IUPAF_WRITEONLY|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "PASSWORD", NULL, NULL, NULL, NULL, IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "APPENDNEWLINE", NULL, NULL, IUPAF_SAMEASSYSTEM, "YES", IUPAF_NOT_MAPPED|IUPAF_NO_INHERIT);

  iupClassRegisterAttribute(ic, "OVERWRITE", NULL, NULL, NULL, NULL, IUPAF_NOT_SUPPORTED|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "FILTER", NULL, NULL, NULL, NULL, IUPAF_NOT_SUPPORTED|IUPAF_NO_INHERIT);
  iupClassRegisterAttribute(ic, "SCROLLVISIBLE", NULL, NULL, NULL, NULL, IUPAF_NOT_SUPPORTED|IUPAF_NO_INHERIT);
}
