[RTL UI] adds bidi.lua, bootstrap UI mirroring with RTL languages

Set default language (for Harfbuzz to pick up localized glyphs
in a font), default text direction, and UI element mirroring
depending on the UI language.
pull/5680/head
poire-z 4 years ago
parent 08a5275984
commit 866c9571df

@ -399,6 +399,37 @@ function FileManagerMenu:setUpdateItemTable()
})
end,
})
table.insert(self.menu_items.developer_options.sub_item_table, {
text = "UI layout mirroring and text direction",
sub_item_table = {
{
text = _("Reverse UI layout mirroring"),
checked_func = function()
return G_reader_settings:isTrue("dev_reverse_ui_layout_mirroring")
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_reverse_ui_layout_mirroring")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end
},
{
text = _("Reverse UI text direction"),
checked_func = function()
return G_reader_settings:isTrue("dev_reverse_ui_text_direction")
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_reverse_ui_text_direction")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end
}
}
})
self.menu_items.cloud_storage = {
text = _("Cloud storage"),

@ -0,0 +1,189 @@
--[[--
Bidirectional text and UI mirroring setup and helpers.
There are 2 concepts we attempt to handle:
- Text direction: Left-To-Right (LTR) or Right-To-Left (RTL)
- UI elements mirroring: not-mirrored, or mirrored
These 2 concepts are somehow orthogonal to each other in
their implementation, even if in the real world there are
only 2 valid combinations:
- LTR and not-mirrored: for western languages, CJK, Indic...
- RTL and mirrored: for Arabic, Hebrew, Farsi and a few others.
Text direction is handled by the libkoreader-xtext.so C module,
and the TextWidget and TextBoxWidget widgets that handle text
aligment. We just need here to set the default global paragraph
direction (that widgets can override if needed).
UI mirroring is to be handled by our widget themselves, with the
help of a few functions defined here.
Fortunately, low level widgets like LeftContainer, RightContainer,
FrameContainer, HorizontalGroup, OverlapGroup... will do most of
the work.
But some care must be taken in other widgets and apps when:
- some arrow symbols are used (for next, previous, first, last...):
they might need to be swapped, or some alternative symbols or
images can be used.
- some geometry arithmetic is done (e.g. detecting if a tap is on the
right part of screen, to go forward), which need to be adapted/reversed.
- handling left or right swipe, whose action might need to be reversed
- some TextBoxWidget/InputText might need to be forced to be LTR (when
showing HTML or CSS code, or entering URLs, path...)
Some overview at:
https://material.io/design/usability/bidirectionality.html
]]
local Language = require("ui/language")
local _ = require("gettext")
local Bidi = {
_mirrored_ui_layout = false,
_rtl_ui_text = false,
}
-- Setup UI mirroring and RTL text for UI language
function Bidi.setup(lang)
local is_rtl = Language:isLanguageRTL(lang)
-- Mirror UI if language is RTL
Bidi._mirrored_ui_layout = is_rtl
-- Unless requested not to (or requested mirroring with LTR language)
if G_reader_settings:isTrue("dev_reverse_ui_layout_mirroring") then
Bidi._mirrored_ui_layout = not Bidi._mirrored_ui_layout
end
-- Xtext default language and direction
if G_reader_settings:nilOrTrue("use_xtext") then
local xtext = require("libs/libkoreader-xtext")
-- Text direction should normally not follow ui mirroring
-- lang override (so that Arabic is still right aligned
-- when one wants the UI layout LTR). But allow it to
-- be independantly reversed (for testing UI mirroring
-- with english text right aligned).
if G_reader_settings:isTrue("dev_reverse_ui_text_direction") then
is_rtl = not is_rtl
end
Bidi._rtl_ui_text = is_rtl
xtext.setDefaultParaDirection(is_rtl)
-- Text language: this helps picking localized glyphs from the
-- font (eg. ideographs shaped differently for Japanese vs
-- Simplified Chinese vs Traditional Chinese).
-- Allow overriding xtext language rules from main UI language
-- (eg. English UI, with French line breaking rules)
local alt_lang = G_reader_settings:readSetting("xtext_alt_lang") or lang
if alt_lang then
xtext.setDefaultLang(alt_lang)
end
end
-- Optimise Bidi.default and Bidi.wrap by aliasing them to the right wrappers
if Bidi._rtl_ui_text then
Bidi.default = Bidi.rtl
Bidi.wrap = Bidi.rtl
else
Bidi.default = Bidi.ltr
Bidi.wrap = Bidi.noop
end
end
-- Use this function in widgets to check if UI elements mirroring
-- is to be done
function Bidi.mirroredUILayout()
return Bidi._mirrored_ui_layout
end
-- This function might only be useful in some rare cases (RTL text
-- is handled directly by TextWidget and TextBoxWidget)
function Bidi.rtlUIText()
return Bidi._rtl_ui_text
end
-- Small helper to mirror gesture directions
local mirrored_directions = {
east = "west",
west = "east",
northeast = "northwest",
northwest = "northeast",
southeast = "southwest",
southwest = "southeast",
}
function Bidi.flipDirectionIfMirroredUILayout(direction)
if Bidi._mirrored_ui_layout then
return mirrored_directions[direction] or direction
end
return direction
end
function Bidi.flipIfMirroredUILayout(bool)
if Bidi._mirrored_ui_layout then
return not bool
end
return bool
end
-- Wrap provided text with bidirectionality control characters, see:
-- http://unicode.org/reports/tr9/#Markup_And_Formatting
-- https://www.w3.org/International/questions/qa-bidi-unicode-controls.en
-- https://www.w3.org/International/articles/inline-bidi-markup/
-- This works only when use_xtext=true: these characters are used
-- by FriBidi for correct char visual ordering, and later stripped
-- by Harfbuzz.
-- When use_xtext=false, these characters are considered as normal
-- characters, and would be printed. Fortunately, most fonts know them
-- and provide an invisible glyph of zero-width - except FreeSans and
-- FreeSerif which provide a real glyph (a square with "LRI" inside)
-- which would be an issue and would need stripping. But as these
-- Free fonts are only used as fallback fonts, and the invisible glyphs
-- will have been found in the previous fonts, we don't need to.
local LRI = "\xE2\x81\xA6" -- U+2066 LRI / LEFT-TO-RIGHT ISOLATE
local RLI = "\xE2\x81\xA7" -- U+2067 RLI / RIGHT-TO-LEFT ISOLATE
local FSI = "\xE2\x81\xA8" -- U+2068 FSI / FIRST STRONG ISOLATE
local PDI = "\xE2\x81\xA9" -- U+2069 PDI / POP DIRECTIONAL ISOLATE
function Bidi.ltr(text)
return string.format("%s%s%s", LRI, text, PDI)
end
function Bidi.rtl(text) -- should hardly be needed
return string.format("%s%s%s", RLI, text, PDI)
end
function Bidi.auto(text) -- from first strong character
return string.format("%s%s%s", FSI, text, PDI)
end
function Bidi.default(text) -- default direction
return Bidi._rtl_ui_text and Bidi.rtl(text) or Bidi.ltr(text)
end
function Bidi.noop(text) -- no wrap
return text
end
-- Helper for concatenated string bits of numbers an symbols (like
-- our reader footer) to keep them ordered in RTL UI (to not have
-- a letter B for battery make the whole string considered LTR).
-- Note: it will be replaced and aliased to Bidi.noop or Bidi.rtl
-- by Bibi.setup() as an optimisation
function Bidi.wrap(text)
return Bidi._rtl_ui_text and Bidi.rtl(text) or text
end
-- See at having GetText_mt.__call() wrap untranslated strings in Bidi.ltr()
-- so they are fully displayed LTR.
-- Use these specific wrappers when the wrapped content type is known
-- (so we can easily switch to use rtl() if RTL readers prefer filenames
-- shown as real RTL).
-- Note: when the filename or path are standalone in a TextWidget, it's
-- better to use "para_direction_rtl = false" without any wrapping.
Bidi.filename = Bidi.ltr
Bidi.directory = Bidi.ltr
Bidi.path = Bidi.ltr
Bidi.url = Bidi.ltr
return Bidi

@ -1,7 +1,5 @@
-- high level wrapper module for gettext
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
local Language = {
@ -47,13 +45,46 @@ local Language = {
zh_TW = "中文(台灣)",
["zh_TW.Big5"] = "中文台灣Big5",
},
-- Languages that are written RTL, and should have the UI mirrored.
-- Should match lang tags defined in harfbuzz/src/hb-ot-tag-table.hh.
-- https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code
-- Not included are those absent or commented out in hb-ot-tag-table.hh.
languages_rtl = {
ar = true, -- Arabic
arz = true, -- Egyptian Arabic
ckb = true, -- Sorani (Central Kurdish)
dv = true, -- Divehi
fa = true, -- Persian
he = true, -- Hebrew
ks = true, -- Kashmiri
ku = true, -- Kurdish
ps = true, -- Pashto
sd = true, -- Sindhi
ug = true, -- Uyghur
ur = true, -- Urdu
yi = true, -- Yiddish
}
}
function Language:getLanguageName(lang_locale)
return self.language_names[lang_locale] or lang_locale
end
function Language:isLanguageRTL(lang_locale)
if not lang_locale then
return false
end
local lang = lang_locale
local sep = lang:find("_")
if sep then
lang = lang:sub(1, sep-1)
end
return self.languages_rtl[lang] or false
end
function Language:changeLanguage(lang_locale)
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
_.changeLang(lang_locale)
G_reader_settings:saveSetting("language", lang_locale)
UIManager:show(InfoMessage:new{

@ -30,6 +30,8 @@ io.stdout:flush()
G_reader_settings = require("luasettings"):open(
DataStorage:getDataDir().."/settings.reader.lua")
local lang_locale = G_reader_settings:readSetting("language")
-- Allow quick switching to Arabic for testing RTL/UI mirroring
if os.getenv("KO_RTL") then lang_locale = "ar_AA" end
local _ = require("gettext")
if lang_locale then
_.changeLang(lang_locale)
@ -147,6 +149,13 @@ SettingsMigration:migrateSettings(G_reader_settings)
local CanvasContext = require("document/canvascontext")
CanvasContext:init(Device)
-- UI mirroring for RTL languages, and text shaping configuration
local Bidi = require("ui/bidi")
Bidi.setup(lang_locale)
-- Avoid loading UIManager and widgets before here, as they may
-- cache Bidi mirroring settings. Check that with:
-- for name, _ in pairs(package.loaded) do print(name) end
-- User fonts override
local fontmap = G_reader_settings:readSetting("fontmap")
if fontmap ~= nil then

Loading…
Cancel
Save