Adding RTL to YuHex — Part 3 of 3
The columns were in their RTL positions. Address digits retained their order. If that had been the end, this would have been a much shorter project.
But a user does more than look at the screen. They type, select, and scroll. Those operations are still in progress at the moment the layout changes.
This final part follows those unfinished operations, then looks at help text containing both Arabic prose and code. Both exposed details that the phrase “make it right-to-left” had hidden from me.
What if the user has typed only A
A byte takes two hexadecimal digits. While entering AB, there is a moment after A and before B. A single hexadecimal digit represents four bits, commonly called a nibble.
Recreating the editor and restoring only its document and cursor offset would not necessarily restore “the next digit goes into the low nibble.” Selection anchors and editing history also carry information beyond the visible cursor position.
YuHex therefore keeps the same HexView and native window handle, HWND, while changing the layout provider. This is the beginning of the switch in HexViewDirection.cpp:
if (rightToLeft == IsEditorRightToLeft() || IsClipboardOperationInProgress()) return;
if (IsEditorRightToLeft()) rtlHorizontalOffset_ = state_.horizontalOffset;
else ltrHorizontalOffset_ = state_.horizontalOffset;
StopSelectionDrag();
directionLayout_ = rightToLeft ? &GetRtlHexViewDirectionLayout() : &GetLtrHexViewDirectionLayout();
The early return rejects redundant changes and switching during clipboard operations. The current horizontal position is stored in the slot belonging to the old direction. StopSelectionDrag() ends an active drag before the provider changes.
Stopping a drag does not mean clearing the selection. A temporary interaction and the state it has produced are separate things.
Move the scrollbar before calculating the viewport
The next step changes the side of the vertical scrollbar. The custom editor does not use WS_EX_LAYOUTRTL to mirror its entire coordinate system. It toggles WS_EX_LEFTSCROLLBAR instead:
LONG_PTR style = GetWindowLongPtrW(hwnd_, GWL_EXSTYLE);
style = rightToLeft ? style | WS_EX_LEFTSCROLLBAR : style & ~WS_EX_LEFTSCROLLBAR;
SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, style);
SetWindowPos(hwnd_, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
SetWindowLongPtrW() updates the extended style. SetWindowPos() with SWP_FRAMECHANGED applies the frame change, while the other flags avoid moving, resizing, activating, or changing the window’s stacking order.
The horizontal position is chosen after that operation, using the resulting client width:
state_.horizontalOffset = rightToLeft
? rtlHorizontalOffset_.value_or(directionLayout_->InitialHorizontalOffset(GetMaxHorizontalOffset()))
: ltrHorizontalOffset_;
UpdateScrollInfo();
rtlHorizontalOffset_ has no value before the first RTL visit. In that case, the provider supplies the initial position: the right edge. Later visits can restore the remembered RTL position. Returning to LTR restores its own position.
Restoration alone can still hide the byte being edited in a narrow window. After the intervening comments, the real code calls EnsureCursorVisible(). It keeps the restored position when the cursor is visible and adjusts it when necessary.
A reader at the edge and a reader in the middle
Resizing creates a related decision. The RTL provider uses this rule:
int ClampHorizontalOffset(int offset, int previousMaximum, int maximum) const override
{
return offset == previousMaximum ? maximum : std::clamp(offset, 0, maximum);
}
Suppose the previous maximum offset was 600 and the user was at 600. If the new maximum is 900, the result becomes 900: the user stays at the right edge.
If the user was at 200, the position stays 200, provided it is still within the new range. std::clamp() only brings out-of-range values back inside it. The previous maximum tells us whether the user was following the edge or had moved into the middle.
The LTR provider retains its earlier clamping policy. I did not add this right-edge-following behavior to the ordinary LTR path.
Extra room for a glyph also needs extra scroll range
The text column is another place where a convincing initial screenshot is insufficient. A wide glyph at the end of a row needs room before the neighboring hex column.
The RTL provider reserves two text cells’ worth of extra space in a 16-byte row:
static float GlyphRoom(float textWidth) { return textWidth / 8.0f; }
Because textWidth represents 16 cells, dividing by eight gives two cells. The room scales with the font rather than being a fixed physical-pixel allowance.
Adding it only to placement could leave content unreachable in a narrow window. The scrolling width therefore includes the same room:
float HorizontalContentWidth(float hexWidth, float textWidth, float gap) const override
{
return hexWidth + gap + textWidth + GlyphRoom(textWidth);
}
The glyph allowance and the ordinary column gap serve different purposes. The clipping rectangle permits drawing into the former, while keeping glyphs out of the hex column. Placement, clipping, and scrolling need to agree about the extra width.
Right-aligned code can still read left to right
YuHex has a language setting and an application direction setting. These are also distinct from temporarily switching only the editor layout.
Arabic is marked as an RTL language. Application-wide RTL is unchecked in this screenshot.
In an Arabic help window, prose needs RTL reading order. Search expressions and code need to retain their own order.
The RTL help uses Windows RichEdit, a text control with paragraph formatting. Instead of inserting display-only control characters into the help text, RtlSearchHelp.cpp assigns paragraph properties:
PARAFORMAT2 paragraph{};
paragraph.cbSize = sizeof(paragraph);
paragraph.dwMask = PFM_RTLPARA | PFM_ALIGNMENT;
paragraph.wEffects = (code || fence || example) ? 0 : PFE_RTLPARA;
paragraph.wAlignment = PFA_RIGHT;
SendMessageW(text, EM_SETPARAFORMAT, 0, reinterpret_cast<LPARAM>(¶graph));
PFM_RTLPARA selects the paragraph-direction property, and PFM_ALIGNMENT selects alignment. code tracks a fenced code block, fence identifies a delimiter line, and example recognizes a particular kind of ASCII-starting list example.
Those paragraphs do not receive the RTL paragraph effect. Other paragraphs receive PFE_RTLPARA. Both receive PFA_RIGHT, so prose and code share a right edge while retaining different reading directions.
An expression such as 0x2A + 4 can sit on the right without reversing its internal reading order. Alignment answers where the text sits. Paragraph direction helps answer how mixed content is arranged.
This is a small parser for the help resource’s conventions, not a general Markdown implementation. It also does not isolate every technical term embedded inside a prose paragraph.
On the web, markup such as <bdi dir="ltr">…</bdi> can isolate an opposite-direction phrase from surrounding text. W3C explains that approach. It is useful background, but it would be inaccurate to say that YuHex already applies equivalent isolation everywhere.
What a test can tell me
Address order is one property I can check without reading Arabic. EditorDirectionTests.cpp checks addresses with eight and sixteen digits, A–F, and the final colon. Require() is a test helper that fails when its condition is false:
Require(ltr==address&&rtl==address&&rtl.back()==L':',"RTL address must preserve LTR digits and punctuation");
The test source also compares the extracted LTR provider against the original layout calculation. Correcting RTL must not quietly change the familiar LTR geometry.
For behavioral checks, I keep the file and encoding the same and change only direction. Does a click select the same offset? Do copied data and saved bytes agree? Does switching preserve nibble input and Undo/Redo? Narrow windows and different scaling settings matter as well.
These questions are different from “does the Arabic look plausible to me?” They establish properties of an editor, rather than linguistic naturalness.
Native-speaker review of the translations and fine layout details is still outstanding. Mixed technical phrases also leave room for improvement. Passing mechanical checks does not erase those limits.
YuHex is available from the Microsoft Store and as a free edition, including for commercial use.
I am still not used to reading from the right. What has changed is that I can now describe more precisely why my familiar arrangement might be awkward for someone else.
I happily start at the right-hand column of a Japanese novel, yet an interface that starts on the right still makes me hesitate. Apparently, my idea of normal depends on which window I have open.
On the other side of a language I cannot read is someone examining files and getting their work done. They do not want to lose a half-entered byte. They want a click to select the byte they can see. We read in different directions, but those expectations are the same. I want this tool to preserve that ordinary ease first.

Top comments (0)