riched20: Implement EM_FORMATRANGE and add test cases

Lei Zhang thestig at google.com
Sun Jun 10 04:33:24 CDT 2007


Hi,

Troy Rollo wrote this patch well over a year ago, but it never got
accepted. I updated it with a conformance test and made a small
modification so it passes the conformance test.
-------------- next part --------------
From e5782c40c7bb2e0b92718bc47cf3f61fed31f367 Mon Sep 17 00:00:00 2001
From: Lei Zhang <thestig at google.com>
Date: Sun, 10 Jun 2007 02:27:49 -0700
Subject: [PATCH] riched20: Implement EM_FORMATRANGE and add test cases

---
 dlls/riched20/context.c      |   16 ++
 dlls/riched20/editor.c       |    6 -
 dlls/riched20/editor.h       |    9 +
 dlls/riched20/editstr.h      |   26 +++
 dlls/riched20/list.c         |  154 ++++++++++++++++++++
 dlls/riched20/paint.c        |  316 ++++++++++++++++++++++++++++++++++++++----
 dlls/riched20/run.c          |   42 ++++--
 dlls/riched20/style.c        |  117 +++++++++++-----
 dlls/riched20/tests/editor.c |   38 +++++
 dlls/riched20/wrap.c         |   19 ++-
 10 files changed, 664 insertions(+), 79 deletions(-)

diff --git a/dlls/riched20/context.c b/dlls/riched20/context.c
index 7eef556..5ca22e5 100644
--- a/dlls/riched20/context.c
+++ b/dlls/riched20/context.c
@@ -24,6 +24,22 @@ void ME_InitContext(ME_Context *c, ME_Te
 {
   c->nSequence = editor->nSequence++;  
   c->hDC = hDC;
+  c->hdcMeasure = hDC;
+  c->fr.hdc = hDC;
+  c->fr.hdcTarget = hDC;
+  c->fr.rc.left = 0;
+  c->fr.rc.top = ME_GetYScrollPos(editor);
+  c->fr.rc.right = editor->sizeWindow.cx;
+  c->fr.rc.bottom = editor->sizeWindow.cy + ME_GetYScrollPos(editor);
+  c->fr.rcPage = c->fr.rc;
+  c->fr.chrg.cpMin = 0;
+  c->fr.chrg.cpMax = -1;
+  c->bClip = FALSE;
+  c->bHideSelection = FALSE;
+  c->dDraw.cx = GetDeviceCaps(hDC, LOGPIXELSX);
+  c->dDraw.cy = GetDeviceCaps(hDC, LOGPIXELSY);
+  c->dTarget = c->dDraw;
+  c->mRender = 0;
   c->editor = editor;
   c->pt.x = 0;
   c->pt.y = 0;
diff --git a/dlls/riched20/editor.c b/dlls/riched20/editor.c
index b83fb41..597c1dc 100644
--- a/dlls/riched20/editor.c
+++ b/dlls/riched20/editor.c
@@ -39,7 +39,7 @@
   + EM_FINDTEXTEX (only FR_DOWN flag implemented)
   - EM_FINDWORDBREAK
   - EM_FMTLINES
-  - EM_FORMATRANGE
+  + EM_FORMATRANGE
   + EM_GETAUTOURLDETECT 2.0
   - EM_GETBIDIOPTIONS 3.0
   - EM_GETCHARFORMAT (partly done)
@@ -198,7 +198,6 @@
  * - horizontal scrolling (not even started)
  * - hysteresis during wrapping (related to scrollbars appearing/disappearing)
  * - find/replace
- * - how to implement EM_FORMATRANGE and EM_DISPLAYBAND ? (Mission Impossible)
  * - italic caret with italic fonts
  * - IME
  * - most notifications aren't sent at all (the most important ones are)
@@ -1430,7 +1429,6 @@ static LRESULT RichEditWndProc_common(HW
   UNSUPPORTED_MSG(EM_DISPLAYBAND)
   UNSUPPORTED_MSG(EM_FINDWORDBREAK)
   UNSUPPORTED_MSG(EM_FMTLINES)
-  UNSUPPORTED_MSG(EM_FORMATRANGE)
   UNSUPPORTED_MSG(EM_GETBIDIOPTIONS)
   UNSUPPORTED_MSG(EM_GETEDITSTYLE)
   UNSUPPORTED_MSG(EM_GETIMECOMPMODE)
@@ -2287,6 +2285,8 @@ static LRESULT RichEditWndProc_common(HW
     }
     return MAKELONG( pt.x, pt.y );
   }
+  case EM_FORMATRANGE:
+    return ME_FormatContent(editor, (FORMATRANGE *) lParam, (BOOL) wParam);
   case WM_CREATE:
     if (GetWindowLongW(hWnd, GWL_STYLE) & WS_HSCROLL)
     { /* Squelch the default horizontal scrollbar it would make */
diff --git a/dlls/riched20/editor.h b/dlls/riched20/editor.h
index f71842b..f4e10d4 100644
--- a/dlls/riched20/editor.h
+++ b/dlls/riched20/editor.h
@@ -73,6 +73,9 @@ CHARFORMAT2W *ME_ToCFAny(CHARFORMAT2W *t
 void ME_CopyToCFAny(CHARFORMAT2W *to, CHARFORMAT2W *from);
 void ME_CopyCharFormat(CHARFORMAT2W *pDest, CHARFORMAT2W *pSrc); /* only works with 2W structs */
 void ME_CharFormatFromLogFont(HDC hDC, LOGFONTW *lf, CHARFORMAT2W *fmt); /* ditto */
+ME_Style *ME_MapStyle(ME_StyleMap *m, ME_Style *s);
+ME_StyleMap *ME_MakeStyleMap(void);
+void   ME_FreeStyleMap(ME_StyleMap *m);
 
 /* list.c */
 void ME_InsertBefore(ME_DisplayItem *diWhere, ME_DisplayItem *diWhat);
@@ -87,6 +90,7 @@ void ME_DestroyDisplayItem(ME_DisplayIte
 void ME_DestroyTableCellList(ME_DisplayItem *item);
 void ME_DumpDocument(ME_TextBuffer *buffer);
 const char *ME_GetDITypeName(ME_DIType type);
+void ME_DuplicateText(ME_DisplayItem *pFirst, ME_DisplayItem *pLast, ME_DisplayItem **ppNew, ME_DisplayItem **ppNewLast);
 
 /* string.c */
 int ME_GetOptimalBuffer(int nLen);
@@ -149,8 +153,8 @@ ME_DisplayItem *ME_InsertRunAtCursor(ME_
                                      ME_Style *style, const WCHAR *str, int len, int flags);
 void ME_CheckCharOffsets(ME_TextEditor *editor);
 void ME_PropagateCharOffset(ME_DisplayItem *p, int shift);
-void ME_GetGraphicsSize(ME_TextEditor *editor, ME_Run *run, SIZE *pSize);
-int ME_CharFromPoint(ME_TextEditor *editor, int cx, ME_Run *run);
+void ME_GetGraphicsSize(ME_TextEditor *editor, ME_Run *run, SIZE *pSize, ME_Dimension *pd);
+int ME_CharFromPoint(ME_Context *c, int cx, ME_Run *run);
 /* this one accounts for 1/2 char tolerance */
 int ME_CharFromPointCursor(ME_TextEditor *editor, int cx, ME_Run *run);
 int ME_PointFromChar(ME_TextEditor *editor, ME_Run *pRun, int nOffset);
@@ -237,6 +241,7 @@ void ME_MarkForPainting(ME_TextEditor *e
 void ME_MarkAllForWrapping(ME_TextEditor *editor);
 
 /* paint.c */
+LPARAM ME_FormatContent(ME_TextEditor *editor, FORMATRANGE *pfr, BOOL bNoOutput);
 void ME_PaintContent(ME_TextEditor *editor, HDC hDC, BOOL bOnlyNew, RECT *rcUpdate);
 void ME_Repaint(ME_TextEditor *editor);
 void ME_RewrapRepaint(ME_TextEditor *editor);
diff --git a/dlls/riched20/editstr.h b/dlls/riched20/editstr.h
index ee5dfe3..22103bb 100644
--- a/dlls/riched20/editstr.h
+++ b/dlls/riched20/editstr.h
@@ -324,9 +324,34 @@ typedef struct tagME_TextEditor
   BOOL bHaveFocus;
 } ME_TextEditor;
 
+typedef struct tagME_Dimension
+{
+	unsigned cx;
+	unsigned cy;
+} ME_Dimension;
+
+typedef struct tagME_StyleMapping
+{
+  ME_Style *from;
+  ME_Style *to;
+  struct tagME_StyleMapping *next;
+} ME_StyleMapping;
+
+typedef struct tagME_StyleMap
+{
+  ME_StyleMapping *first;
+} ME_StyleMap;
+
 typedef struct tagME_Context
 {
   HDC hDC;
+  HDC hdcMeasure;
+  FORMATRANGE fr;
+  BOOL	bClip;
+  BOOL	bHideSelection;
+  ME_Dimension dDraw;
+  ME_Dimension dTarget;
+  ME_StyleMap *mRender;
   POINT pt;
   POINT ptRowOffset;
   RECT rcView;
@@ -352,5 +377,4 @@ typedef struct tagME_WrapContext
   ME_DisplayItem *pLastSplittableRun;
   POINT ptLastSplittableRun;
 } ME_WrapContext;  
-
 #endif
diff --git a/dlls/riched20/list.c b/dlls/riched20/list.c
index ad7b4b6..14db791 100644
--- a/dlls/riched20/list.c
+++ b/dlls/riched20/list.c
@@ -2,6 +2,7 @@
  * RichEdit - Basic operations on double linked lists.
  *
  * Copyright 2004 by Krzysztof Foltman
+ * Copyright 2006 CorVu Corporation
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -126,6 +127,139 @@ void ME_DestroyDisplayItem(ME_DisplayIte
   FREE_OBJ(item);
 }
 
+static void ME_DuplicateTableCellList(ME_DisplayItem *dst, ME_DisplayItem *src);
+
+static ME_DisplayItem *ME_DuplicateDisplayItem(ME_DisplayItem *item,
+                                               ME_DisplayItem *prev,
+                                               ME_StyleMap *m)
+{
+  ME_DisplayItem *mdi = ALLOC_OBJ(ME_DisplayItem);
+
+  *mdi = *item;
+  mdi->prev = prev;
+  mdi->next = 0;
+  if (prev)
+    prev->next = mdi;
+
+  if (item->type == diParagraph || item->type == diUndoSplitParagraph)
+  {
+    ME_DuplicateTableCellList(mdi, item);
+    mdi->member.para.pFmt = ALLOC_OBJ(PARAFORMAT2);
+    *mdi->member.para.pFmt = *item->member.para.pFmt;
+    mdi->member.para.next_para = 0;
+
+    /* Now link up the top level paragraph with its preceding top
+     * level paragraph.
+     */
+    if (mdi->member.para.prev_para)
+    {
+      ME_DisplayItem *from, *to;
+      ME_DisplayItem *target = mdi->member.para.prev_para;
+
+      mdi->member.para.prev_para = 0;
+
+      for (from = item, to = mdi;
+           from && to;
+           from = from->prev, to = to->prev)
+      {
+        if (from == target)
+        {
+          mdi->member.para.prev_para = to;
+          if (to->type == diParagraph || to->type == diUndoSplitParagraph)
+            to->member.para.next_para = mdi;
+          break;
+        }
+      }
+    }
+  }
+
+  if (item->type == diRun || item->type == diUndoInsertRun)
+  {
+    /* We need to copy the style and text, in the case of styles
+     * we need to reduce duplication which we can do with a style
+     * map.
+     */
+    mdi->member.run.style = ME_MapStyle(m, mdi->member.run.style);
+    mdi->member.run.strText = ME_StrDup(item->member.run.strText);
+
+    if (mdi->member.run.pCell)
+    {
+      /* Find the right ME_TableCell to link to by walking both the
+       * old and new list backwards.
+       */
+      ME_DisplayItem *from, *to;
+      ME_TableCell *target = mdi->member.run.pCell;
+
+      mdi->member.run.pCell = 0;
+      for (from = item, to = mdi;
+           !mdi->member.run.pCell && from && to;
+           from = from->prev, to = to->prev)
+      {
+        ME_TableCell *tt, *tf;
+
+	if (from->type != diParagraph && from->type != diUndoSplitParagraph)
+	    continue;
+
+        for (tf = from->member.para.pCells, tt = from->member.para.pCells;
+             tf && tt;
+             tf = tf->next, tt = tt->next)
+        {
+          if (tf == target)
+          {
+            mdi->member.run.pCell = tt;
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  if (item->type == diUndoSetCharFormat || item->type == diUndoSetDefaultCharFormat)
+  {
+    /* Just map the style back */
+    mdi->member.ustyle = ME_MapStyle(m, mdi->member.ustyle);
+  }
+  if (item->type == diTextEnd)
+  {
+    /* Make sure the last top level paragraph is linked up */
+    ME_DisplayItem *from, *to;
+
+    mdi->member.para.prev_para = 0;
+
+    for (from = item, to = mdi;
+         from && to;
+         from = from->prev, to = to->prev)
+    {
+      if ((from->type == diParagraph || from->type == diUndoSplitParagraph) &&
+          from->member.para.next_para == item)
+      {
+        to->member.para.next_para = mdi;
+        break;
+      }
+    }
+  }
+  return mdi;
+}
+
+/* ME_DuplicateText makes a duplicate of all ME_DisplayItem structures starting
+ * with pFirst and ending with pLast.
+ */
+void ME_DuplicateText(ME_DisplayItem *pFirst, ME_DisplayItem *pLast, ME_DisplayItem **ppNew, ME_DisplayItem **ppNewLast)
+{
+  ME_DisplayItem *pNow;
+  ME_StyleMap *m = ME_MakeStyleMap();
+
+  pNow = ME_DuplicateDisplayItem(pFirst, NULL, m);
+  *ppNew = pNow;
+  while (pFirst != pLast)
+  {
+    pFirst = pFirst->next;
+    pNow = ME_DuplicateDisplayItem(pFirst, pNow, m);
+    *ppNewLast = pNow;
+  }
+  ME_FreeStyleMap(m);
+}
+
 void
 ME_DestroyTableCellList(ME_DisplayItem *item)
 {
@@ -143,6 +277,26 @@ ME_DestroyTableCellList(ME_DisplayItem *
   }
 }
 
+static void
+ME_DuplicateTableCellList(ME_DisplayItem *dst,
+                          ME_DisplayItem *src)
+{
+  ME_TableCell **tail = &dst->member.para.pCells;
+  ME_TableCell *cell = src->member.para.pCells;
+
+  *tail = dst->member.para.pLastCell = NULL;
+
+  while (cell)
+  {
+    ME_TableCell *p = ALLOC_OBJ(ME_TableCell);
+
+    p->nRightBoundary = cell->nRightBoundary;
+    *tail = dst->member.para.pLastCell = p;
+    cell = cell->next;
+    tail = &p->next;
+  }
+}
+
 ME_DisplayItem *ME_MakeDI(ME_DIType type) {
   ME_DisplayItem *item = ALLOC_OBJ(ME_DisplayItem);
   ZeroMemory(item, sizeof(ME_DisplayItem));
diff --git a/dlls/riched20/paint.c b/dlls/riched20/paint.c
index 63a0c4c..a1a32ec 100644
--- a/dlls/riched20/paint.c
+++ b/dlls/riched20/paint.c
@@ -3,6 +3,7 @@
  *
  * Copyright 2004 by Krzysztof Foltman
  * Copyright 2005 by Phil Krylov
+ * Copyright 2006 CorVu Corporation
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -23,6 +24,189 @@ #include "editor.h"
 
 WINE_DEFAULT_DEBUG_CHANNEL(richedit);
 
+static int transform_x(ME_Context *c, int x)
+{
+	if (c->dTarget.cx == c->dDraw.cx)
+		return x;
+	return MulDiv(x, c->dDraw.cx, c->dTarget.cx);
+}
+
+static int transform_y(ME_Context *c, int y)
+{
+	if (c->dTarget.cy == c->dDraw.cy)
+		return y;
+	return MulDiv(y, c->dDraw.cy, c->dTarget.cy);
+}
+
+/*
+ * Support EM_FORMATRANGE.
+ *
+ * *** Notes on tables ***
+ *
+ * The CHARRANGE structure passed in the FORMATRANGE structure is not
+ * sufficient to deal with tables, where we would need information on the
+ * start position for text in each column of the table.
+ *
+ * Testing on Windows NT shows that the Microsoft Rich Edit control gives
+ * pathological results when there are tables in the document - both in edit
+ * mode and for EM_FORMATRANGE. The Microsoft Rich Edit control uses the
+ * table solely as a hint to where the paragraph should start. The table
+ * does not expand to fit its text. Long paragraphs wrap when you hit the
+ * right page margin and continue at the left page margin.
+ *
+ * If the paragraph ends outside its cell, the paragraph in the next cell
+ * continues at the X position immediately after the next cell boundary after
+ * the position of the last character in the paragraph. This may not be the
+ * cell associated with the paragraph, and may be the right hand table
+ * boundary (so that the paragraph commences after the right hand edge of the
+ * table). If the paragraph ends after the right hand table boundary, then the
+ * next paragraph begins on the next row of output at the X position for the
+ * second cell in the table.
+ *
+ * While all this wrapping is going on, the table borders will be positioned
+ * as if there were only one line of text in each cell of the table and the
+ * text fits neatly within the cells.
+ *
+ * This behaviour is sufficiently useless that it is probably not worth trying
+ * to duplicate, and given that CHARRANGE is insufficient to deal with tables,
+ * there seems little point in attempting to make our implementation of
+ * EM_FORMATRANGE do something sensible for tables. A lot of effort would be
+ * involved in doing so, and we would be doing something Microsoft has not
+ * done.
+ */
+
+LPARAM ME_FormatContent(ME_TextEditor *editor, FORMATRANGE *pfr, BOOL bDraw)
+{
+  ME_Context c;
+  int	iBGMode;
+  ME_DisplayItem *pFirst;
+  ME_DisplayItem *pLast;
+  ME_DisplayItem *pNow;
+  ME_DisplayItem *pIntra;
+  int	yNow;
+  int	cyRow;
+  int	cyOffset = -1;
+  int	iEndOffset = -1;
+
+  if (!pfr)
+    return ME_GetTextLength(editor);
+
+  ME_InitContext(&c, editor, pfr->hdcTarget);
+  c.fr = *pfr;
+  c.bHideSelection = TRUE;
+  c.bClip = TRUE;
+
+  c.rcView.left = MulDiv(c.fr.rc.left, c.dTarget.cx, 1440);
+  c.rcView.right = MulDiv(c.fr.rc.right, c.dTarget.cx, 1440);
+  c.rcView.top = MulDiv(c.fr.rc.top, c.dTarget.cy, 1440);
+  c.rcView.bottom = MulDiv(c.fr.rc.bottom, c.dTarget.cy, 1440);
+
+  c.pt.x = 0;
+  c.pt.y = 0;
+
+  /* Get a copy of all the text in the control so we can perform calculations
+   * without interfering with the values cached for the on-screen data.
+   */
+
+  ME_DuplicateText(editor->pBuffer->pFirst, editor->pBuffer->pLast, &pFirst, &pLast);
+
+  /* Now perform wrapping on the resulting data, and find the vertical offset of
+   * the row at the start of the character range, and the character offset of the
+   * first non-printable character.
+   */
+
+  for (pNow = pFirst->next; pNow != pLast; pNow = pNow->member.para.next_para)
+  {
+    pNow->member.para.nYPos = c.pt.y;
+    pNow->member.para.nFlags |= MEPF_REWRAP;
+    ME_WrapTextParagraph(&c, pNow);
+
+    if (iEndOffset == -1)
+    {
+      int iParaOfs = pNow->member.para.nCharOfs;
+
+      yNow = c.pt.y;
+      cyRow = 0;
+
+      for (pIntra = pNow; pIntra != pNow->member.para.next_para; pIntra = pIntra->next)
+      {
+        if (pIntra->type == diStartRow)
+        {
+	  yNow += cyRow;
+          cyRow = pIntra->member.row.nHeight;
+        }
+	else if (pIntra->type == diRun)
+	{
+	  int iRunOfs = iParaOfs + pIntra->member.run.nCharOfs;
+	  int iRunEndOfs = iRunOfs + ME_VPosToPos(pIntra->member.run.strText,
+						  pIntra->member.run.strText ?
+						    pIntra->member.run.strText->nLen : 1);
+	  if (cyOffset == -1 && iRunEndOfs > c.fr.chrg.cpMin)
+	    cyOffset = yNow;
+	  if (cyOffset != -1 && iEndOffset == -1)
+	  {
+	    if (yNow > cyOffset &&
+	        yNow + cyRow > c.rcView.bottom - c.rcView.top + cyOffset)
+	    {
+	      iEndOffset = iRunOfs;
+	      break;
+	    }
+	    if (c.fr.chrg.cpMax >= 0 &&
+		iRunEndOfs > c.fr.chrg.cpMax)
+	    {
+	      int iBottom = yNow + cyRow - cyOffset + c.rcView.top;;
+
+	      if (c.rcView.bottom > iBottom)
+		c.rcView.bottom = iBottom;
+	      iEndOffset = iRunOfs;
+	      break;
+	    }
+	  }
+	}
+      }
+    }
+
+    c.pt.y += pNow->member.para.nHeight;
+  }
+
+  if (iEndOffset == -1)
+    iEndOffset = ME_CharOfsFromRunOfs(editor, ME_FindItemBack(pLast, diRun), 0);
+
+  /* Then draw the data */
+  /* FIXME - transformation of non-text runs is not done yet */
+  
+  if (bDraw)
+  {
+    c.mRender = ME_MakeStyleMap();
+    iBGMode = SetBkMode(c.hDC, TRANSPARENT);
+    c.dDraw.cx = GetDeviceCaps(c.fr.hdc, LOGPIXELSX);
+    c.dDraw.cy = GetDeviceCaps(c.fr.hdc, LOGPIXELSY);
+    c.hDC = c.fr.hdc;
+    c.hdcMeasure = c.fr.hdcTarget;
+    c.pt.x = 0;
+    c.pt.y = c.rcView.top - cyOffset;
+
+    for (pNow = pFirst->next; pNow != pLast; pNow = pNow->member.para.next_para)
+    {
+      if (c.pt.y + pNow->member.para.nHeight > c.rcView.top &&
+	  c.pt.y < c.rcView.bottom)
+        ME_DrawParagraph(&c, pNow);
+      c.pt.y += pNow->member.para.nHeight;
+    }
+    SetBkMode(c.hDC, iBGMode);
+    ME_FreeStyleMap(c.mRender);
+  }
+
+  while (pFirst)
+  {
+    pNow = pFirst;
+    pFirst = pFirst->next;
+    ME_DestroyDisplayItem(pNow);
+  }
+
+  return iEndOffset;
+}
+
 void ME_PaintContent(ME_TextEditor *editor, HDC hDC, BOOL bOnlyNew, RECT *rcUpdate) {
   ME_DisplayItem *item;
   ME_Context c;
@@ -141,12 +325,15 @@ ME_RewrapRepaint(ME_TextEditor *editor)
 
 
 static void ME_DrawTextWithStyle(ME_Context *c, int x, int y, LPCWSTR szText, int nChars, 
-  ME_Style *s, int *width, int nSelFrom, int nSelTo, int ymin, int cy) {
+  ME_Style *sIn, int *width, int nSelFrom, int nSelTo, int ymin, int cy) {
+  ME_Style *s = (c->mRender ? ME_MapStyle(c->mRender, sIn) : sIn);
   HDC hDC = c->hDC;
+  HDC hdcTarget = c->hdcMeasure;
+  ME_TextEditor *editor = ((hDC == hdcTarget) ? c->editor : NULL);
   HGDIOBJ hOldFont;
   COLORREF rgbOld, rgbBack;
   int yOffset = 0, yTwipsOffset = 0;
-  hOldFont = ME_SelectStyleFont(c->editor, hDC, s);
+  hOldFont = ME_SelectStyleFont(editor, hDC, s);
   rgbBack = ME_GetBackColor(c->editor);
   if ((s->fmt.dwMask & CFM_LINK) && (s->fmt.dwEffects & CFE_LINK))
     rgbOld = SetTextColor(hDC, RGB(0,0,255));  
@@ -166,14 +353,68 @@ static void ME_DrawTextWithStyle(ME_Cont
     int numerator = 1;
     int denominator = 1;
     
-    if (c->editor->nZoomNumerator)
+    if (editor && editor->nZoomNumerator)
     {
-      numerator = c->editor->nZoomNumerator;
-      denominator = c->editor->nZoomDenominator;
+      numerator = editor->nZoomNumerator;
+      denominator = editor->nZoomDenominator;
     }
     yOffset = yTwipsOffset * GetDeviceCaps(hDC, LOGPIXELSY) * numerator / denominator / 1440;
   }
-  ExtTextOutW(hDC, x, y-yOffset, 0, NULL, szText, nChars, NULL);
+  if (hDC != hdcTarget)
+  {
+    WCHAR *szTmp = ALLOC_N_OBJ(WCHAR, nChars);
+    WCHAR *szGlyphs = ALLOC_N_OBJ(WCHAR, nChars * 2);
+    int *lpDx = ALLOC_N_OBJ(int, nChars * 2);
+    HFONT holdFont = ME_SelectStyleFont(NULL, hdcTarget, sIn);
+    int	i;
+    
+    GCP_RESULTSW gcpr;   
+
+    /* First, deal with any re-ordering in the text */
+
+    memset(&gcpr, 0, sizeof(gcpr));
+    gcpr.lStructSize = sizeof(gcpr);
+    gcpr.nGlyphs = nChars;
+    gcpr.lpOutString = szTmp;
+    GetCharacterPlacementW(hDC, szText, nChars, 0, &gcpr, GCP_REORDER);
+
+    /* Then convert the string to glyph indeces and position according to target */
+
+    gcpr.lpOutString = 0;
+    gcpr.lpGlyphs = szGlyphs;
+    gcpr.lpDx = lpDx;
+    gcpr.nGlyphs = nChars * 2;
+
+    GetCharacterPlacementW(hdcTarget, szTmp, nChars, 0, &gcpr, 0);
+    /* FIXME - the flags should be GCP_DIACRITIC | GCP_GLYPHSHAPE, but
+     * GetCharacterPlacement ignores these and spews FIXME lines for each call.
+     */
+
+    /* Then scale the horizontal positions for WYSIWYG output */
+
+    for (i = 1; i < gcpr.nGlyphs; ++i)
+	lpDx[i] += lpDx[i - 1];
+    for (i = 0; i < gcpr.nGlyphs; ++i)
+    	lpDx[i] = transform_x(c, lpDx[i]);
+    for (i = gcpr.nGlyphs - 1; i > 0; --i)
+    	lpDx[i] -= lpDx[i - 1];
+
+    /* Finally, output the glyphs to the real output device */
+
+    ExtTextOutW(hDC, transform_x(c, x), transform_y(c, y - yOffset),
+		ETO_GLYPH_INDEX, NULL, szGlyphs, gcpr.nGlyphs, lpDx);
+    
+    /* And clean up */
+
+    FREE_OBJ(lpDx);
+    FREE_OBJ(szGlyphs);
+    FREE_OBJ(szTmp);
+    ME_UnselectStyleFont(NULL, hdcTarget, sIn, holdFont);
+  }
+  else
+  {
+    ExtTextOutW(hDC, x, y-yOffset, 0, NULL, szText, nChars, NULL);
+  }
   if (width) {
     SIZE sz;
     GetTextExtentPoint32W(hDC, szText, nChars, &sz);
@@ -189,11 +430,13 @@ static void ME_DrawTextWithStyle(ME_Cont
     GetTextExtentPoint32W(hDC, szText+nSelFrom, nSelTo-nSelFrom, &sz);
     
     /* Invert selection if not hidden by EM_HIDESELECTION */
-    if (c->editor->bHideSelection == FALSE)
+    if (editor && editor->bHideSelection == FALSE && c->bHideSelection == FALSE)
 	PatBlt(hDC, x, ymin, sz.cx, cy, DSTINVERT);
   }
   SetTextColor(hDC, rgbOld);
-  ME_UnselectStyleFont(c->editor, hDC, s, hOldFont);
+  ME_UnselectStyleFont(editor, hDC, s, hOldFont);
+  if (c->mRender)
+    ME_ReleaseStyle(s);
 }
 
 static void ME_DebugWrite(HDC hDC, POINT *pt, WCHAR *szText) {
@@ -210,7 +453,7 @@ static void ME_DrawGraphics(ME_Context *
                             ME_Paragraph *para, BOOL selected) {
   SIZE sz;
   int xs, ys, xe, ye, h, ym, width, eyes;
-  ME_GetGraphicsSize(c->editor, run, &sz);
+  ME_GetGraphicsSize(c->editor, run, &sz, &c->dDraw);
   xs = run->pt.x;
   ys = y-sz.cy;
   xe = xs+sz.cx;
@@ -258,7 +501,8 @@ static void ME_DrawRun(ME_Context *c, in
     return;
 
   if (run->nFlags & MERF_GRAPHICS)
-    ME_DrawGraphics(c, x, y, run, para, (runofs >= nSelFrom) && (runofs < nSelTo));
+    ME_DrawGraphics(c, x, y, run, para,
+		    !c->editor->bHideSelection && !c->bHideSelection && (runofs >= nSelFrom) && (runofs < nSelTo));
   else
   {
     if (c->editor->cPasswordMask)
@@ -292,7 +536,7 @@ void ME_DrawParagraph(ME_Context *c, ME_
   ME_DisplayItem *p;
   ME_Run *run;
   ME_Paragraph *para = NULL;
-  RECT rc, rcPara;
+  RECT rc, rcPara, rcParaDraw;
   int y = c->pt.y;
   int height = 0, baseline = 0, no=0, pno = 0;
   int xs, xe;
@@ -315,22 +559,40 @@ void ME_DrawParagraph(ME_Context *c, ME_
         y += height;
         rcPara.top = y;
         rcPara.bottom = y+p->member.row.nHeight;
-        visible = RectVisible(c->hDC, &rcPara);
+
+	/* A row is visible if:
+	 *
+	 *  Part of the rectangle is visible AND
+	 *  (The bottom part of the row is in the clip range OR
+	 *   Some part of the row is in the clip range and the top starts
+	 *   at or before before the top of the clip range)
+	 */
+
+	rcParaDraw.left = transform_x(c, rcPara.left);
+	rcParaDraw.top = transform_y(c, rcPara.top);
+	rcParaDraw.right = transform_x(c, rcPara.right);
+	rcParaDraw.bottom = transform_y(c, rcPara.bottom);
+
+        visible = RectVisible(c->hDC, &rcParaDraw) &&
+		  (!c->bClip ||
+		   (rcPara.bottom > c->rcView.top &&
+		    (rcPara.bottom <= c->rcView.bottom ||
+		     rcPara.top <= c->rcView.top)));
         if (visible) {
           HBRUSH hbr;
           hbr = CreateSolidBrush(ME_GetBackColor(c->editor));
           /* left margin */
-          rc.left = c->rcView.left;
-          rc.right = c->rcView.left+nMargWidth;
-          rc.top = y;
-          rc.bottom = y+p->member.row.nHeight;
+          rc.left = transform_x(c, c->rcView.left);
+          rc.right = transform_x(c, c->rcView.left+nMargWidth);
+          rc.top = transform_y(c, y);
+          rc.bottom = transform_y(c, y+p->member.row.nHeight);
           FillRect(c->hDC, &rc, hbr/* c->hbrMargin */);
           /* right margin */
-          rc.left = xe;
-          rc.right = c->rcView.right;
+          rc.left = transform_x(c, xe);
+          rc.right = transform_x(c, c->rcView.right);
           FillRect(c->hDC, &rc, hbr/* c->hbrMargin */);
-          rc.left = c->rcView.left+nMargWidth;
-          rc.right = xe;
+          rc.left = transform_x(c, c->rcView.left+nMargWidth);
+          rc.right = transform_x(c, xe);
           FillRect(c->hDC, &rc, hbr);
           DeleteObject(hbr);
         }
@@ -352,10 +614,10 @@ void ME_DrawParagraph(ME_Context *c, ME_
         assert(para);
         run = &p->member.run;
         if (visible && me_debug) {
-          rc.left = c->rcView.left+run->pt.x;
-          rc.right = c->rcView.left+run->pt.x+run->nWidth;
-          rc.top = c->pt.y+run->pt.y;
-          rc.bottom = c->pt.y+run->pt.y+height;
+          rc.left = transform_x(c, c->rcView.left+run->pt.x);
+          rc.right = transform_x(c, c->rcView.left+run->pt.x+run->nWidth);
+          rc.top = transform_y(c, c->pt.y+run->pt.y);
+          rc.bottom = transform_y(c, c->pt.y+run->pt.y+height);
           TRACE("rc = (%d, %d, %d, %d)\n", rc.left, rc.top, rc.right, rc.bottom);
           if (run->nFlags & MERF_SKIPPED)
             DrawFocusRect(c->hDC, &rc);
@@ -363,15 +625,15 @@ void ME_DrawParagraph(ME_Context *c, ME_
             FrameRect(c->hDC, &rc, GetSysColorBrush(COLOR_GRAYTEXT));
         }
         if (visible)
-          ME_DrawRun(c, run->pt.x, c->pt.y+run->pt.y+baseline, p, &paragraph->member.para);
+          ME_DrawRun(c, c->rcView.left+run->pt.x, c->pt.y+run->pt.y+baseline, p, &paragraph->member.para);
         if (me_debug)
         {
           /* I'm using %ls, hope wsprintfW is not going to use wrong (4-byte) WCHAR version */
           const WCHAR wszRunDebug[] = {'[','%','d',':','%','x',']',' ','%','l','s',0};
           WCHAR buf[2560];
           POINT pt;
-          pt.x = run->pt.x;
-          pt.y = c->pt.y + run->pt.y;
+          pt.x = transform_x(c, run->pt.x);
+          pt.y = transform_y(c, c->pt.y + run->pt.y);
           wsprintfW(buf, wszRunDebug, no, p->member.run.nFlags, p->member.run.strText->szData);
           ME_DebugWrite(c->hDC, &pt, buf);
         }
diff --git a/dlls/riched20/run.c b/dlls/riched20/run.c
index 29fc309..e9bc7a8 100644
--- a/dlls/riched20/run.c
+++ b/dlls/riched20/run.c
@@ -473,11 +473,26 @@ void ME_UpdateRunFlags(ME_TextEditor *ed
  * Sets run extent for graphics runs. This functionality is just a placeholder
  * for future OLE object support, and will be removed.
  */     
-void ME_GetGraphicsSize(ME_TextEditor *editor, ME_Run *run, SIZE *pSize)
+void ME_GetGraphicsSize(ME_TextEditor *editor, ME_Run *run, SIZE *pSize, ME_Dimension *pd)
 {
+  ME_Dimension d;
+
   assert(run->nFlags & MERF_GRAPHICS);
-  pSize->cx = 64;
-  pSize->cy = 64;
+  if (pd)
+  {
+    d = *pd;
+  }
+  else
+  {
+    HDC hdc = GetDC(editor->hWnd);
+
+    d.cx = GetDeviceCaps(hdc, LOGPIXELSX);
+    d.cy = GetDeviceCaps(hdc, LOGPIXELSY);
+    ReleaseDC(editor->hWnd, hdc);
+  }
+  /* Return a square 1/3rd of an inch wide */
+  pSize->cx = 2 * d.cx / 3;
+  pSize->cy = 2 * d.cy / 3;
 }
 
 /******************************************************************************
@@ -487,7 +502,7 @@ void ME_GetGraphicsSize(ME_TextEditor *e
  * pixel horizontal position. This version rounds left (ie. if the second
  * character is at pixel position 8, then for cx=0..7 it returns 0).  
  */     
-int ME_CharFromPoint(ME_TextEditor *editor, int cx, ME_Run *run)
+int ME_CharFromPoint(ME_Context *c, int cx, ME_Run *run)
 {
   int fit = 0;
   HGDIOBJ hOldFont;
@@ -505,17 +520,17 @@ int ME_CharFromPoint(ME_TextEditor *edit
   if (run->nFlags & MERF_GRAPHICS)
   {
     SIZE sz;
-    ME_GetGraphicsSize(editor, run, &sz);
+    ME_GetGraphicsSize(c->editor, run, &sz, &c->dDraw);
     if (cx < sz.cx)
       return 0;
     return 1;
   }
-  hDC = GetDC(editor->hWnd);
-  hOldFont = ME_SelectStyleFont(editor, hDC, run->style);
+  hDC = c->hDC;
+  hOldFont = ME_SelectStyleFont(c->editor, hDC, run->style);
   
-  if (editor->cPasswordMask)
+  if (c->editor->cPasswordMask)
   {
-    ME_String *strMasked = ME_MakeStringR(editor->cPasswordMask,ME_StrVLen(run->strText));
+    ME_String *strMasked = ME_MakeStringR(c->editor->cPasswordMask,ME_StrVLen(run->strText));
     GetTextExtentExPointW(hDC, strMasked->szData, run->strText->nLen,
       cx, &fit, NULL, &sz);
     ME_DestroyString(strMasked);
@@ -526,8 +541,7 @@ int ME_CharFromPoint(ME_TextEditor *edit
       cx, &fit, NULL, &sz);
   }
   
-  ME_UnselectStyleFont(editor, hDC, run->style, hOldFont);
-  ReleaseDC(editor->hWnd, hDC);
+  ME_UnselectStyleFont(c->editor, hDC, run->style, hOldFont);
   return fit;
 }
 
@@ -563,7 +577,7 @@ int ME_CharFromPointCursor(ME_TextEditor
   if (run->nFlags & MERF_GRAPHICS)
   {
     SIZE sz;
-    ME_GetGraphicsSize(editor, run, &sz);
+    ME_GetGraphicsSize(editor, run, &sz, NULL);
     if (cx < sz.cx/2)
       return 0;
     return 1;
@@ -614,7 +628,7 @@ int ME_PointFromChar(ME_TextEditor *edit
   if (pRun->nFlags & MERF_GRAPHICS)
   {
     if (!nOffset) return 0;
-    ME_GetGraphicsSize(editor, pRun, &size);
+    ME_GetGraphicsSize(editor, pRun, &size, NULL);
     return 1;
   }
   
@@ -706,7 +720,7 @@ static SIZE ME_GetRunSizeCommon(ME_Conte
   }
   if (run->nFlags & MERF_GRAPHICS)
   {
-    ME_GetGraphicsSize(c->editor, run, &size);
+    ME_GetGraphicsSize(c->editor, run, &size, &c->dDraw);
     if (size.cy > *pAscent)
       *pAscent = size.cy;
     /* descent is unchanged */
diff --git a/dlls/riched20/style.c b/dlls/riched20/style.c
index 1dfdef1..ef19306 100644
--- a/dlls/riched20/style.c
+++ b/dlls/riched20/style.c
@@ -323,48 +323,57 @@ HFONT ME_SelectStyleFont(ME_TextEditor *
   LOGFONTW lf;
   int i, nEmpty, nAge = 0x7FFFFFFF;
   ME_FontCacheItem *item;
+  int	nZoomNumerator = editor ? editor->nZoomNumerator : 1;
+  int	nZoomDenominator = editor ? editor->nZoomDenominator : 1;
   assert(hDC);
   assert(s);
   
-  ME_LogFontFromStyle(hDC, &lf, s, editor->nZoomNumerator, editor->nZoomDenominator);
+  ME_LogFontFromStyle(hDC, &lf, s, nZoomNumerator, nZoomDenominator);
   
-  for (i=0; i<HFONT_CACHE_SIZE; i++)
-    editor->pFontCache[i].nAge++;
-  for (i=0, nEmpty=-1, nAge=0; i<HFONT_CACHE_SIZE; i++)
+  if (editor)
   {
-    item = &editor->pFontCache[i];
-    if (!item->nRefs)
+    for (i=0; i<HFONT_CACHE_SIZE; i++)
+      editor->pFontCache[i].nAge++;
+    for (i=0, nEmpty=-1, nAge=0; i<HFONT_CACHE_SIZE; i++)
     {
-      if (item->nAge > nAge)
-        nEmpty = i, nAge = item->nAge;
+      item = &editor->pFontCache[i];
+      if (!item->nRefs)
+      {
+        if (item->nAge > nAge)
+          nEmpty = i, nAge = item->nAge;
+      }
+      if (item->hFont && ME_IsFontEqual(&item->lfSpecs, &lf))
+        break;
     }
-    if (item->hFont && ME_IsFontEqual(&item->lfSpecs, &lf))
-      break;
-  }
-  if (i < HFONT_CACHE_SIZE) /* found */
-  {
-    item = &editor->pFontCache[i];
-    TRACE_(richedit_style)("font reused %d\n", i);
+    if (i < HFONT_CACHE_SIZE) /* found */
+    {
+      item = &editor->pFontCache[i];
+      TRACE_(richedit_style)("font reused %d\n", i);
 
-    s->hFont = item->hFont;
-    item->nRefs++;
+      s->hFont = item->hFont;
+      item->nRefs++;
+    }
+    else
+    {
+      item = &editor->pFontCache[nEmpty]; /* this legal even when nEmpty == -1, as we don't dereference it */
+
+      assert(nEmpty != -1); /* otherwise we leak cache entries or get too many fonts at once*/
+      if (item->hFont) {
+        TRACE_(richedit_style)("font deleted %d\n", nEmpty);
+        DeleteObject(item->hFont);
+        item->hFont = NULL;
+      }
+      s->hFont = CreateFontIndirectW(&lf);
+      assert(s->hFont);
+      TRACE_(richedit_style)("font created %d\n", nEmpty);
+      item->hFont = s->hFont;
+      item->nRefs = 1;
+      memcpy(&item->lfSpecs, &lf, sizeof(LOGFONTW));
+    }
   }
-  else
+  else if (!s->hFont)
   {
-    item = &editor->pFontCache[nEmpty]; /* this legal even when nEmpty == -1, as we don't dereference it */
-
-    assert(nEmpty != -1); /* otherwise we leak cache entries or get too many fonts at once*/
-    if (item->hFont) {
-      TRACE_(richedit_style)("font deleted %d\n", nEmpty);
-      DeleteObject(item->hFont);
-      item->hFont = NULL;
-    }
     s->hFont = CreateFontIndirectW(&lf);
-    assert(s->hFont);
-    TRACE_(richedit_style)("font created %d\n", nEmpty);
-    item->hFont = s->hFont;
-    item->nRefs = 1;
-    memcpy(&item->lfSpecs, &lf, sizeof(LOGFONTW));
   }
   hOldFont = SelectObject(hDC, s->hFont);
   /* should be cached too, maybe ? */
@@ -379,6 +388,8 @@ void ME_UnselectStyleFont(ME_TextEditor 
   assert(hDC);
   assert(s);
   SelectObject(hDC, hOldFont);
+  if (!editor)
+    return;
   for (i=0; i<HFONT_CACHE_SIZE; i++)
   {
     ME_FontCacheItem *pItem = &editor->pFontCache[i];
@@ -423,6 +434,50 @@ void ME_ReleaseStyle(ME_Style *s)
     ME_DestroyStyle(s);
 }
 
+ME_Style *ME_MapStyle(ME_StyleMap *m, ME_Style *s)
+{
+  ME_StyleMapping **tail;
+
+  if (!s)
+    return s;
+
+  for (tail = &m->first; *tail; tail = &(*tail)->next)
+  {
+    if ((*tail)->from == s)
+    {
+      ME_AddRefStyle((*tail)->to);
+      return (*tail)->to;
+    }
+  }
+  *tail = ALLOC_OBJ(ME_StyleMapping);
+  (*tail)->from = s;
+  (*tail)->to = ME_MakeStyle(&s->fmt);
+  ME_AddRefStyle((*tail)->to);
+  (*tail)->next = 0;
+  return (*tail)->to;
+}
+
+ME_StyleMap *ME_MakeStyleMap(void)
+{
+  ME_StyleMap *sm = ALLOC_OBJ(ME_StyleMap);
+
+  sm->first = 0;
+  return sm;
+}
+
+void ME_FreeStyleMap(ME_StyleMap *m)
+{
+  while (m->first)
+  {
+    ME_StyleMapping *p = m->first;
+
+    m->first = p->next;
+    ME_ReleaseStyle(p->to);
+    FREE_OBJ(p);
+  }
+  FREE_OBJ(m);
+}
+
 ME_Style *ME_GetInsertStyle(ME_TextEditor *editor, int nCursor) {
   if (ME_IsSelection(editor))
   {
diff --git a/dlls/riched20/tests/editor.c b/dlls/riched20/tests/editor.c
index 1881005..e93d45d 100644
--- a/dlls/riched20/tests/editor.c
+++ b/dlls/riched20/tests/editor.c
@@ -1491,6 +1491,43 @@ static void test_WM_PASTE(void)
     DestroyWindow(hwndRichEdit);
 }
 
+static void test_EM_FORMATRANGE(void)
+{
+  int r;
+  FORMATRANGE fr;
+  HDC hdc;
+  HWND hwndRichEdit = new_richedit(NULL);
+
+  SendMessage(hwndRichEdit, WM_SETTEXT, 0, (LPARAM) haystack);
+
+  hdc = GetDC(hwndRichEdit);
+  ok(hdc != NULL, "Could not get HDC\n");
+
+  fr.hdc = fr.hdcTarget = hdc;
+  fr.rc.top = fr.rcPage.top = fr.rc.left = fr.rcPage.left = 0;
+  fr.rc.right = fr.rcPage.right = GetDeviceCaps(hdc, HORZRES);
+  fr.rc.bottom = fr.rcPage.bottom = GetDeviceCaps(hdc, VERTRES);
+  fr.chrg.cpMin = 0;
+  fr.chrg.cpMax = 20;
+
+  r = SendMessage(hwndRichEdit, EM_FORMATRANGE, TRUE, (LPARAM) NULL);
+  ok(r == 31, "EM_FORMATRANGE expect %d, got %d\n", 31, r);
+
+  r = SendMessage(hwndRichEdit, EM_FORMATRANGE, TRUE, (LPARAM) &fr);
+  ok(r == 20, "EM_FORMATRANGE expect %d, got %d\n", 20, r);
+
+  fr.chrg.cpMin = 0;
+  fr.chrg.cpMax = 10;
+
+  r = SendMessage(hwndRichEdit, EM_FORMATRANGE, TRUE, (LPARAM) &fr);
+  ok(r == 10, "EM_FORMATRANGE expect %d, got %d\n", 10, r);
+
+  r = SendMessage(hwndRichEdit, EM_FORMATRANGE, TRUE, (LPARAM) NULL);
+  ok(r == 31, "EM_FORMATRANGE expect %d, got %d\n", 31, r);
+
+  DestroyWindow(hwndRichEdit);
+}
+
 static int nCallbackCount = 0;
 
 static DWORD CALLBACK EditStreamCallback(DWORD_PTR dwCookie, LPBYTE pbBuff,
@@ -1808,6 +1845,7 @@ START_TEST( editor )
   test_EM_EXSETSEL();
   test_WM_PASTE();
   test_EM_StreamIn_Undo();
+  test_EM_FORMATRANGE();
   test_unicode_conversions();
 
   /* Set the environment variable WINETEST_RICHED20 to keep windows
diff --git a/dlls/riched20/wrap.c b/dlls/riched20/wrap.c
index af6e9a9..c3e5d70 100644
--- a/dlls/riched20/wrap.c
+++ b/dlls/riched20/wrap.c
@@ -168,7 +168,7 @@ static ME_DisplayItem *ME_SplitByBacktra
   int i, idesp, len;
   ME_Run *run = &p->member.run;
 
-  idesp = i = ME_CharFromPoint(wc->context->editor, loc, run);
+  idesp = i = ME_CharFromPoint(wc->context, loc, run);
   len = ME_StrVLen(run->strText);
   assert(len>0);
   assert(i<len);
@@ -367,6 +367,23 @@ void ME_WrapTextParagraph(ME_Context *c,
   for (p = tp->next; p!=tp->member.para.next_para; ) {
     assert(p->type != diStartRow);
     if (p->type == diRun) {
+      int iStartOffset = tp->member.para.nCharOfs + p->member.run.nCharOfs;
+      int iEndOffset = iStartOffset + ME_VPosToPos(p->member.run.strText,
+		      				p->member.run.strText ?
+		      				p->member.run.strText->nLen : 0);
+
+      if (iStartOffset < c->fr.chrg.cpMin && iEndOffset > c->fr.chrg.cpMin)
+      {
+        ME_SplitRun(c, p, ME_PosToVPos(p->member.run.strText, c->fr.chrg.cpMin - iStartOffset));
+      } else if (iStartOffset < c->fr.chrg.cpMax && iEndOffset > c->fr.chrg.cpMax)
+      {
+        ME_SplitRun(c, p, ME_PosToVPos(p->member.run.strText, c->fr.chrg.cpMax - iStartOffset));
+      }
+      if ((iStartOffset == c->fr.chrg.cpMin || iStartOffset == c->fr.chrg.cpMax) &&
+	  wc.pRowStart && wc.pRowStart != p)
+      {
+        ME_InsertRowStart(&wc, p);
+      }
       p = ME_WrapHandleRun(&wc, p);
       continue;
     }
-- 
1.4.2.1


More information about the wine-patches mailing list