Adding RTL to YuHex — Part 2 of 3
First, a checklist for RTL work
For a Windows application, adding RTL support is primarily work on the UI and help. But it is more than moving the visible elements: clicks and keystrokes must still reach the data the user sees.
I would begin with the following checklist. These are tasks to assess and verify for an application, not a claim that YuHex has already completed every item.
- [ ] Define the scope. Separate application UI direction from document or editor direction. Decide which regions become RTL and which remain LTR.
- [ ] Review windows and dialogs. Check menus, toolbars, labels, fields, buttons, and alignment, including which child windows inherit a parent's layout.
- [ ] Check separately opened UI. Include settings, search, confirmations, and error messages.
- [ ] Separate text direction from alignment. Test RTL prose mixed with LTR letters, numbers, brackets, and punctuation. Right alignment alone does not establish RTL direction.
- [ ] Identify elements that must retain their meaning. Preserve appropriate orientation and internal order for logos, images containing text, numbers, paths, addresses, hex values, and code. Evaluate navigation arrows according to their function.
- [ ] Review custom drawing. Check column widths, margins, origins, clipping, fonts, and joined glyphs. Avoid applying both custom and Windows mirroring to the same content unintentionally.
- [ ] Keep coordinates tied to data. Verify that clicks, drags, selections, and the caret correspond to the visible character or byte.
- [ ] Check keyboard behavior. Review arrow keys, Home/End, tab order, shortcuts, and selection extension according to each field's role and reading direction.
- [ ] Preserve in-progress input. A direction change or redraw must not lose an edited value or partially entered input.
- [ ] Vary display conditions. Check resizing, horizontal scrolling, narrow windows, and DPI or display scaling.
- [ ] Verify data invariants. RTL layout alone must not change byte order, copied content, saved results, or Undo/Redo results.
- [ ] Review help. Set paragraph direction separately from alignment. Keep code, paths, and expressions in the required LTR order; test links and search too.
- [ ] Test language and direction changes. Check redraws, already-open windows, and restored settings. Language selection and layout direction are separate decisions.
- [ ] Retest LTR. Compare the same files and operations against the original behavior. Ask readers of the target language to evaluate whether the layout feels natural.
Windows provides RTL window layout support, but it is not one switch that settles every surface. Microsoft's documentation distinguishes layout, inheritance, image mirroring, and coordinate mapping.
Part 2 focuses on custom drawing and the mapping between screen coordinates and data in YuHex. Part 3 follows in-progress input and help. The checklist is a way to organize the work, not a certification that every item is complete.
Part 1 used a short Arabic sentence to separate grammar, paragraph direction, and the order of numbers. A hex editor adds another question: what exactly should move when its layout changes?
In YuHex, I rearrange the columns. The bytes retain their order. If those two decisions become entangled, the screen might look plausible while a click selects a different byte from the one under the pointer.
I wanted to keep that possibility out of the design as far as I could. Here is how the actual C++ fits together.
Two arrangements of the same file
These screenshots show the same 912-byte sample. I changed only the editor area’s direction; the interface language remains English.
Address, hex, and text columns from left to right. The first byte is 59.
Text, hex, and address columns from left to right. The first bytes remain 59 75 48 65.
The text column prioritizes correspondence with byte positions. Rearranging the columns for RTL is separate from typesetting a natural Arabic paragraph as a word processor would.
The address column moves to the right. The digits and trailing colon in 00000010: must retain their order. I had to revisit and fix that detail even after the broader RTL work. Where a field sits and how its contents are ordered need separate answers.
Design goal: limit the impact on existing LTR behavior
Before working on RTL placement, I wanted a structure that would help preserve the LTR display and editing behavior already in use. If every adjustment to RTL spacing also required changing LTR code, the area that needed checking would keep growing. I used the following principles to decide where each change belonged.
Keep document and editing state separate from layout. The existing editor continues to own the bytes, selection, partially entered hexadecimal digit, and undo/redo state. Changing direction replaces the layout provider without recreating the editor or its window. There is no need to transfer an editing session to another view every time the arrangement changes.
Retain existing LTR behavior and separate RTL-specific implementations. Column layout has a shared interface; the LTR implementation calls the existing calculation function, while RTL calculations live in a separate class and source file. For dialogs, I also used direction-specific entry points that continue to the existing screen for LTR and delegate RTL work to a dedicated implementation. Connecting these paths still changes existing code, but an RTL spacing correction can stay local instead of requiring a rewrite of the LTR formulas and screens.
Choose direction near creation and switching boundaries. I avoided adding separate LTR and RTL calculations independently to drawing, clicking, and dragging. The connection code selects a layout provider, and the following operations obtain positions through the shared interface. Drawing and hit testing must refer to the same layout result.
Treat application direction and editor direction separately. Menu and dialog direction is separate from the arrangement of byte columns. The comparison images above keep the English interface and change only the editor area. That area uses explicit column positions rather than window-wide mirroring. Keeping digit and byte order, along with the ordinary coordinate system, limits how much existing cell calculations and input handling need to change.
Check LTR regressions alongside RTL behavior. Introducing a shared interface changes the LTR call path too. I check display, clicking, and selection with the same data, as well as resizing, DPI, horizontal scrolling, and direction changes during partial input. Separating the implementations limits the reach of a change; testing checks the connections between them.
The column calculations below show how these design choices appear in the code.
Put the direction decision at a boundary
The original LTR editor already had a function for calculating column positions. I kept that calculation and introduced separate LTR and RTL layout classes with a common interface, HexViewDirectionLayout.
These objects calculate geometry. They do not own the document or selection. This excerpt from HexViewDirectionLayout.h shows some of the questions they answer:
virtual HexViewRowLayout Build(float margin, float y, float lineHeight, float addressWidth,
float hexWidth, float textWidth, float gap, float clientRight, int horizontalOffset) const = 0;
virtual float HexCursorLeft(int byte, float charWidth, float textWidth, float gap) const = 0;
virtual float HorizontalContentWidth(float hexWidth, float textWidth, float gap) const = 0;
Build() returns rectangles for the columns. HexCursorLeft() locates the cursor within the scrolling content, and HorizontalContentWidth() calculates that content’s width. Grouping those decisions lets the editor replace its layout provider without replacing its document.
The LTR provider delegates to the existing calculation. This is its Build() implementation in LtrHexViewDirectionLayout.cpp:
HexViewRowLayout Build(float margin, float y, float lineHeight, float addressWidth,
float hexWidth, float textWidth, float gap, float clientRight, int offset) const override
{
return BuildHexViewRowLayout(margin, y, lineHeight, addressWidth, hexWidth, textWidth, gap, clientRight, offset);
}
An RTL spacing correction therefore does not require changing the LTR formula. The common interface still changes the call path, so I also need regression checks. Keeping changes separate reduces their reach; it does not prove that existing behavior is unchanged.
Reserve the address column on the right
The RTL provider first reserves a fixed address column, then uses the space to its left for the scrolling bytes. In RtlHexViewDirectionLayout.cpp:
const float addressLeft = std::max(margin, clientRight - addressWidth);
layout.addressRect = D2D1::RectF(addressLeft, y, std::max(addressLeft, clientRight), y + lineHeight);
const float viewportRight = std::max(margin, addressLeft - gap);
clientRight is the right boundary, addressWidth is the address column’s width, and gap separates columns. Subtracting the address width gives its left edge. Subtracting the gap gives viewportRight, the right boundary available to scrolling byte content.
For example, use a margin of 8, a right boundary of 1,000, an address width of 128, and a gap of 12. The address occupies 872–1,000, and the byte viewport ends at 860. These are device-independent pixels, or DIPs, so the geometry can be used independently of physical display scaling.
The following consecutive lines place the text and hex columns:
const float glyphRoom = GlyphRoom(textWidth);
layout.contentLeft = margin + std::max(0.0f, viewportRight - margin - textWidth - glyphRoom - gap - hexWidth)
- static_cast<float>(offset);
layout.textLeft = layout.contentLeft;
layout.textRight = layout.textLeft + textWidth;
layout.hexLeft = layout.textRight + glyphRoom + gap;
layout.hexRight = layout.hexLeft + hexWidth;
With a text width of 128, a hex width of 384, glyph room of 16, and scroll offset zero, the result is:
| Region | Left | Right |
|---|---|---|
| Text | 320 | 448 |
| Glyph room and column gap | 448 | 476 |
| Hex | 476 | 860 |
| Address | 872 | 1,000 |
The std::max(0.0f, …) term pushes the content toward the right when there is room, without creating negative padding when the window is narrow. Subtracting offset then scrolls that content leftward. The fixed address rectangle was calculated separately, so it stays put.
The coordinate system itself retains its ordinary meaning: larger x values are farther right. Bytes within the relocated hex column still progress from left to right.
A click should undo the drawing calculation
Both drawing and hit testing obtain their column positions through the same directionLayout_->Build() interface. They do not each invent their own RTL transform.
In HexViewLayout.cpp, a hex cell’s rectangle is calculated like this:
const float left = layout.hexLeft + static_cast<float>(byteIndex) * GetHexByteCellWidth(charWidth);
return D2D1::RectF(left, layout.y, left + GetHexByteCellWidth(charWidth), layout.y + lineHeight);
byteIndex is the byte’s index within its row. GetHexByteCellWidth() returns three times the character width: two hexadecimal digits plus spacing. With an 8-DIP character width, a byte occupies 24 DIPs.
Now consider the reverse operation. This is the hex-column branch of HitTest() in HexView.cpp:
if (x >= rowLayout.hexLeft && x < rowLayout.hexRight)
{
byteIndex = static_cast<int>((x - rowLayout.hexLeft) / GetHexByteCellWidth(hexCharWidth_));
}
Subtract the column’s left edge from the click position, then divide by the cell width. With the example above, a click at x=512 gives (512 - 476) / 24 = 1.5. Converting to an integer gives byte index 1. On a row beginning at offset 0x20, that selects 0x21.
Drawing adds an indexed cell width to the column origin. Hit testing subtracts that origin and divides by the width. Once both use the same layout, the calculation does not need a separate rule that reverses RTL byte indexes.
Before this branch, the real code also rejects clicks outside the byte viewport. That prevents selecting bytes hidden underneath the fixed address column. A correct formula still needs a correct domain.
Why I did not mirror the entire editor
Windows provides WS_EX_LAYOUTRTL, an extended window style that affects the horizontal coordinate origin and the direction in which coordinates increase. It is a standard facility described in the Microsoft documentation.
For YuHex’s custom editor area, I chose explicit placement instead. I wanted to rearrange columns while retaining the existing relationship between cell coordinates and input. This is a choice for this codebase, rather than a general instruction to avoid native RTL layout.
There is a cost: my code must keep column widths, clipping, scrolling, and hit testing consistent. Introducing an interface does not make those responsibilities disappear.
The user may be halfway through an edit
We now know where the columns go and how a click finds a byte. The columns move, but the selected data stays the same. The visible layout and the operation finally agree.
The tool shown here is available as a Microsoft Store edition and a free edition, including for commercial use. Looking at a familiar file in both arrangements makes the distinction easier to see.
From my side, it may be just a display change. From the user's side, work is already in progress. They may have entered one hex digit, or just found the offset they needed in a narrow window. Changing the view should not make them start that work again.
Can I keep that half-entered byte while switching the layout? Can resizing preserve the place they were looking at? Part 3 follows the code that keeps the work going while the screen changes. The help window has another twist: right-aligned prose and code still need different reading directions.


Top comments (0)