Compare commits

..
103 changed files with 1154 additions and 2767 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ Every screenshot below is WordCraft itself, rendered offscreen by its own UI tes
| **References** | Table of contents, footnotes and endnotes, citations and bibliography (APA, MLA, Chicago, IEEE), captions, table of figures, cross-references, index, table of authorities |
| **Review** | Spelling and grammar with suggestions (English and Polish, by each run's language), thesaurus, word count, comments in margin balloons or a pane, track changes, accept/reject, compare documents, restrict editing, accessibility checker, document inspector |
| **Mailings** | Mail merge from CSV, merge fields, address block, greeting line, rules, preview, finish to a document; envelopes and labels |
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български, Tiếng Việt, 한국어, Svenska, Hrvatski, Nederlands, Dansk, Română, العربية (mirrored) or Slovenščina (follows the system language by default) |
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български, Tiếng Việt, 한국어, Svenska, Hrvatski or Slovenščina (follows the system language by default) |
| **Files** | .docx read/write, legacy 97-2003 .doc import, PDF export, .odt, .rtf, .html, .md, .tex, .txt import/export, page images |
The honest picture, area by area, is in [ROADMAP.md](ROADMAP.md), the
+3 -6
View File
File diff suppressed because one or more lines are too long
-5
View File
@@ -228,11 +228,6 @@ fn block_cost(b: &Block) -> (usize, bool) {
.saturating_add(p.objects.len().saturating_mul(std::mem::size_of::<InlineObject>()));
let objects = p.objects.iter().fold(0usize, |n, o| n.saturating_add(object_cost(o)));
cost = cost.saturating_add(p.text.len()).saturating_add(items).saturating_add(objects);
// The formatting a clone copies too: style ids, font names and the like in the runs,
// the mark and the paragraph's properties.
let runs = p.runs.iter().filter(|r| !r.props.is_empty()).fold(0usize, |n, r| n.saturating_add(serialized_len(&r.props)));
let props = serialized_len(&p.props).saturating_add(serialized_len(&p.mark)).saturating_add(p.section.as_ref().map_or(0, serialized_len));
cost = cost.saturating_add(runs).saturating_add(props);
boxes |= box_ids(p).next().is_some();
});
(cost, boxes)
-18
View File
@@ -415,24 +415,6 @@ fn pasted_box_copies_are_capped_in_bytes() {
assert_eq!(got.iter().flatten().count(), added);
}
#[test]
fn pasted_box_copies_count_the_formatting_they_copy() {
// A story holding a text box and a run whose font name is 1 MiB: each copy clones the
// formatting too, so 64 copies used to copy 64 MiB of it past the 32 MiB cap.
let props = CharProps { font: Some("f".repeat(1 << 20)), ..CharProps::default() };
let mut p = Paragraph::with_text("x", props);
p.insert_object(0, box_shape(Some(999)), &CharProps::default()).unwrap();
let f = fan_out(vec![para_block(p)], 64);
let mut d = doc(&["x"]);
let before = d.parts.len();
d.insert_fragment(&Pos::body(0, 1), &f).unwrap();
let added = d.parts.len() - before;
let font = |p: &Paragraph| p.runs.iter().map(|r| r.props.font.as_ref().map_or(0, String::len)).sum::<usize>();
let copied: usize = d.parts.values().flat_map(|p| p.blocks.iter()).map(|b| if let Block::Para(p) = &**b { font(p) } else { 0 }).sum();
assert!((1..64).contains(&added), "{added}");
assert!(copied <= MAX_PASTED_BOX_BYTES, "{copied}");
}
#[test]
fn pasted_box_copies_count_the_objects_they_copy() {
// #3614 review: a story holding a text box and a 1 MiB kept-for-round-trip object costs
+5 -7
View File
@@ -364,11 +364,6 @@ pub fn read_with_password(bytes: &[u8], password: Option<&str>) -> Result<Docume
// Sections: each section's properties attach to the paragraph that ends it (its story of
// header/footer parts included); the section reaching the end of the text is the final one.
let mut paras = walk(&ctx, 0, fib.ccp.text, &mut pics);
// The first paragraph ending at each CP, found once rather than by a scan per section.
let mut ending: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
for (i, p) in paras.iter().enumerate() {
ending.entry(p.end_cp).or_insert(i);
}
let last = sects.last().map(|s| s.props.clone());
for (si, sec) in sects.iter().enumerate() {
let mut props = sec.props.clone();
@@ -379,8 +374,11 @@ pub fn read_with_password(bytes: &[u8], password: Option<&str>) -> Result<Docume
if is_final {
continue; // handled below through `last`
}
if let Some(p) = ending.get(&sec.end_cp).and_then(|&i| paras.get_mut(i)) {
p.para.section = Some(Box::new(props));
for p in paras.iter_mut() {
if p.end_cp == sec.end_cp {
p.para.section = Some(Box::new(props));
break;
}
}
}
if let Some(mut l) = last {
+8 -26
View File
@@ -101,12 +101,6 @@ impl RowInfo {
self.widths.iter().rfind(|(k, _)| *k == i).map(|(_, w)| *w)
}
/// How many per-cell overrides the row keeps, for tests.
#[cfg(test)]
pub(crate) fn overrides(&self) -> usize {
self.vmerge.len() + self.shading.len() + self.widths.len() + self.width_pcts.len() + self.valign.len() + self.merges.len()
}
/// The width percentage cell `i` ends up with, for tests.
#[cfg(test)]
pub(crate) fn width_pct_of(&self, i: u8) -> Option<f32> {
@@ -114,15 +108,6 @@ impl RowInfo {
}
}
/// Set cell `c`'s override to `v`, replacing an earlier one: one slot per cell, however often
/// a PAPX repeats the sprm, so a shared PAPX can't pile up copies on every paragraph mark.
fn set<T>(slots: &mut Vec<(u8, T)>, c: u8, v: T) {
match slots.iter_mut().find(|(k, _)| *k == c) {
Some(slot) => slot.1 = v,
None => slots.push((c, v)),
}
}
pub(crate) fn decode(papx: &[u8]) -> RowInfo {
let mut r = RowInfo::default();
let mut gap_half: Option<i16> = None;
@@ -159,10 +144,8 @@ pub(crate) fn decode(papx: &[u8]) -> RowInfo {
T_DXA_GAP_HALF => gap_half = s16(&prl),
T_TABLE_BORDERS_80 => r.borders = table_borders(prl.operand),
T_MERGE => {
// Rows apply only the first MAX_COLS merges.
if let [a, b, ..] = prl.operand
&& b > a
&& r.merges.len() < MAX_COLS
{
r.merges.push((*a, *b));
}
@@ -175,8 +158,7 @@ pub(crate) fn decode(papx: &[u8]) -> RowInfo {
1 => VMerge::Continue,
_ => VMerge::None,
};
// The first override for a cell wins.
if m != VMerge::None && !r.vmerge.iter().any(|(k, _)| k == itc) {
if m != VMerge::None {
r.vmerge.push((*itc, m));
}
}
@@ -186,7 +168,7 @@ pub(crate) fn decode(papx: &[u8]) -> RowInfo {
&& let [a, b, ..] = prl.operand.get(1..3).unwrap_or(&[])
{
for c in *a..(*b).min(MAX_COLS as u8) {
set(&mut r.shading, c, Some(rgb));
r.shading.push((c, Some(rgb)));
}
}
}
@@ -194,7 +176,7 @@ pub(crate) fn decode(papx: &[u8]) -> RowInfo {
let cells = prl.operand.get(1..).unwrap_or(&[]);
for (i, b) in cells.as_chunks::<2>().0.iter().take(MAX_COLS).enumerate() {
if let Some(rgb) = shd80_rgb(b) {
set(&mut r.shading, i as u8, Some(rgb));
r.shading.push((i as u8, Some(rgb)));
}
}
}
@@ -214,7 +196,7 @@ pub(crate) fn decode(papx: &[u8]) -> RowInfo {
if raw && b.get(8..10) == Some(&[0xFF, 0xFF]) {
continue;
}
set(&mut r.shading, (first + i) as u8, shd_rgb(b));
r.shading.push(((first + i) as u8, shd_rgb(b)));
}
}
T_CELL_WIDTH => {
@@ -225,12 +207,12 @@ pub(crate) fn decode(papx: &[u8]) -> RowInfo {
if *fts == 3 {
let w = (i16::from_le_bytes([*w0, *w1]) as f32 / 20.0).clamp(0.0, 1584.0);
for c in *a..(*b).min(MAX_COLS as u8) {
set(&mut r.widths, c, w);
r.widths.push((c, w));
}
} else if *fts == 2 {
let pct = (u16::from_le_bytes([*w0, *w1]) as f32 / 50.0).clamp(0.0, 100.0);
for c in *a..(*b).min(MAX_COLS as u8) {
set(&mut r.width_pcts, c, pct);
r.width_pcts.push((c, pct));
}
}
}
@@ -242,8 +224,8 @@ pub(crate) fn decode(papx: &[u8]) -> RowInfo {
2 => VAlign::Bottom,
_ => VAlign::Top,
};
for c in *a..(*b).min(MAX_COLS as u8) {
set(&mut r.valign, c, v);
for c in *a..*b {
r.valign.push((c, v));
}
}
}
-43
View File
@@ -1018,32 +1018,6 @@ fn two_sections_box_the_first() {
assert!(!doc.last_section.title_page);
}
#[test]
fn sections_ending_off_a_paragraph_mark_attach_nowhere() {
// Hostile section ends that miss every paragraph mark are skipped; the one that ends a
// paragraph still boxes it. Paragraphs are found by their end, not scanned per section.
let mut sections: Vec<(u32, Vec<u8>)> = (1..6).map(|cp| (cp, Vec::new())).collect();
sections.push((6, grpprl(&[(0x300A, &[0x01])])));
sections.push((12, Vec::new()));
let spec = StructSpec {
text: "first\rsecond\r",
header_stories: vec![],
sections,
chpx_runs: vec![(0, Vec::new())],
papx_entries: vec![(0, 0, Vec::new()), (6, 0, Vec::new())],
plf_lst: Vec::new(),
plf_lfo: Vec::new(),
note_stories: vec![],
endnote_stories: vec![],
extra_pairs: Vec::new(),
data_stream: Vec::new(),
};
let doc = read(&struct_doc(&spec)).expect("opens");
let paras: Vec<_> = doc.body.iter().filter_map(|b| b.as_para()).collect();
assert!(paras.first().and_then(|p| p.section.as_ref()).expect("first paragraph boxed").title_page);
assert_eq!(paras.iter().filter(|p| p.section.is_some()).count(), 1, "the missing ends attach nowhere");
}
#[test]
fn header_story_becomes_part() {
let spec = StructSpec {
@@ -2401,23 +2375,6 @@ fn second_table_gets_its_own_grid() {
assert_eq!(tables[1].props.indent, None, "indent reset");
}
#[test]
fn repeated_cell_sprms_keep_one_override_per_cell() {
// A PAPX repeating cell-range sprms (shared by every paragraph mark that names it) keeps one
// slot per cell, the last value winning, instead of a copy per repetition.
let mut sprms = Vec::new();
for w in 0..200u16 {
let [w0, w1] = (w * 20).to_le_bytes();
sprms.push(sprm_cb(0xD635, &[0, 63, 3, w0, w1]));
sprms.push(sprm_cb(0xD62C, &[0, 255, 1]));
sprms.push([0x24, 0x56, 0, 2].to_vec());
}
let row = crate::table::decode(&sprms.concat());
assert_eq!(row.width_of(0), Some(199.0), "the last width wins");
assert_eq!(row.width_of(62), Some(199.0));
assert!(row.overrides() <= 63 + 255 + 63, "{}", row.overrides());
}
#[test]
fn sprm_t_cell_width_twips_and_percentage() {
// [MS-DOC] §2.9.290 sprmTCellWidth (0xD635):
+1 -3
View File
@@ -56,9 +56,7 @@ fn craft_fonts(dir: &std::path::Path) -> Result<(String, Vec<String>), String> {
let [family, style, file, scripts, ..] = f.as_slice() else {
return Err(format!("malformed manifest line: {line}"));
};
// Arabic interface faces are small, so the web build keeps them too.
let small_ui_face = scripts.split(',').any(|s| matches!(s.trim(), "Arab" | "Geor"));
if wasm && !small_ui_face {
if wasm && !scripts.split(',').any(|s| s.trim() == "Geor") {
continue;
}
let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?;
+1 -41
View File
@@ -24,10 +24,7 @@ pub use kurbo::BezPath;
pub use script::{fallback_rank, is_emoji, needs_layout_tables, script_fallbacks};
use skrifa::MetadataProvider;
use skrifa::instance::Size;
pub use ui_fallback::{
SystemUiFont, is_cjk, system_arabic_ui_font, system_cjk_ui_font, system_cjk_ui_font_for, system_hant_ui_font, ui_needs_system_cjk,
ui_uncovered_cjk,
};
pub use ui_fallback::{SystemUiFont, is_cjk, system_cjk_ui_font, system_cjk_ui_font_for, system_hant_ui_font, ui_needs_system_cjk, ui_uncovered_cjk};
/// A font from the optional craft-fonts build input (https://github.com/storytold/craft-fonts;
/// empty unless built with `CRAFT_FONTS_DIR`, see `build.rs`).
@@ -101,18 +98,6 @@ pub fn ui_cjk_fonts_for(prefer_hans: bool, prefer_korean: bool) -> Vec<&'static
}
}
/// The craft-fonts faces for Arabic script (`"Arab"`), in manifest order: the document fallback
/// for Arabic text. Empty without craft-fonts.
pub fn arabic_fonts() -> impl Iterator<Item = &'static CraftFont> {
CRAFT_FONTS.iter().filter(|f| f.scripts.contains(&"Arab"))
}
/// The craft-fonts faces for Arabic script (`"Arab"`), Regular styles only: the interface's
/// fallback for an Arabic interface and for Arabic file names. Empty without craft-fonts.
pub fn ui_arabic_fonts() -> Vec<&'static CraftFont> {
arabic_fonts().filter(|f| f.style == "Regular").collect()
}
/// The bundled serif used when a document asks for nothing better.
pub const DEFAULT_FAMILY: &str = "Source Serif 4";
@@ -529,31 +514,6 @@ mod tests {
assert!(fb.family.contains("Mincho"), "{fb:?}");
}
/// Arabic text in a document needs no system font: the craft-fonts Arabic face is the
/// fallback (the browser build has no other), and its letters join when shaped.
#[test]
fn craft_fonts_arabic_face_is_the_document_fallback_for_arabic() {
if arabic_fonts().next().is_none() {
eprintln!("skipped: built without a craft-fonts Arabic face (set CRAFT_FONTS_DIR to a checkout)");
return;
}
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
let latin = db.face(DEFAULT_FAMILY, "Regular");
for ch in "ابتثجحخدذرزسشصضطظعغفقكلمنهويءآأإةى،؛؟٠١٢٣٤٥٦٧٨٩ًٌٍَُِّْ".chars() {
assert!(!latin.covers(ch), "the bundled serif has no Arabic");
let fb = db.fallback_for(ch, latin.id()).unwrap_or_else(|| panic!("no fallback for {ch}"));
assert!(fb.family.contains("Arabic"), "{ch}: {}", fb.family);
}
let face = db.fallback_for('ع', latin.id()).unwrap();
// Joined forms: the same letter gets a different glyph alone and inside a word.
let alone = shape(&face, "ع", &[], |c| c);
let joined = shape(&face, "ععع", &[], |c| c);
assert_eq!((alone.len(), joined.len()), (1, 3));
assert!(joined.iter().all(|g| g.gid != 0));
assert!(joined.iter().any(|g| g.gid != alone[0].gid), "letters take their joining forms");
}
#[test]
fn east_asian_run_uses_one_face_covering_the_whole_run() {
let db = FontDb::with_font_dirs(Vec::new());
-33
View File
@@ -218,36 +218,6 @@ pub fn system_cjk_ui_font_for(hans: bool, korean: bool, needed: &[char]) -> Opti
find_ui_font(&with_asset_data(crate::system_font_dirs()), &candidates(hans, korean), needed, Region::from_hans(hans))
}
/// Arabic font files to look for, relative to a font folder, best first.
fn arabic_candidates() -> &'static [&'static str] {
if cfg!(windows) {
&["segoeui.ttf", "tahoma.ttf", "arial.ttf"]
} else if cfg!(target_os = "macos") {
&["SFArabic.ttf", "GeezaPro.ttc", "Supplemental/GeezaPro.ttc", "Supplemental/Arial Unicode.ttf", "Supplemental/Arial.ttf"]
} else {
&[
"noto/NotoSansArabic-Regular.ttf",
"truetype/noto/NotoSansArabic-Regular.ttf",
"google-noto/NotoSansArabic-Regular.ttf",
"google-noto-sans-arabic-vf-fonts/NotoSansArabic[wght].ttf",
"noto/NotoNaskhArabic-Regular.ttf",
"truetype/noto/NotoNaskhArabic-Regular.ttf",
"google-noto/NotoNaskhArabic-Regular.ttf",
"vazirmatn/Vazirmatn-Regular.ttf",
"truetype/dejavu/DejaVuSans.ttf",
"dejavu/DejaVuSans.ttf",
"TTF/DejaVuSans.ttf",
]
}
}
/// An installed font with Arabic letters, for an Arabic interface in a build whose craft-fonts
/// input has no Arabic face. Reads at most one font file per candidate; never panics on damaged
/// files.
pub fn system_arabic_ui_font() -> Option<SystemUiFont> {
find_ui_font(&crate::system_font_dirs(), arabic_candidates(), &['ع'], Region::Other)
}
/// `dirs` and, for each macOS font asset folder among them (`com_apple_MobileAsset_Font<N>`),
/// the `AssetData` folder of every `<hash>.asset` in it: macOS ships PingFang there, e.g.
/// `/System/Library/AssetsV2/com_apple_MobileAsset_Font7/<hash>.asset/AssetData/PingFang.ttc`
@@ -454,9 +424,6 @@ mod tests {
// No file maps every needed character: the one mapping the most is used.
let found = find_ui_font(&dirs, &["sub/good.ttf", "PingFang.ttc"], &['A', '页'], Region::Hans).unwrap();
assert_eq!(found.family, "Source Sans 3");
// The bundled Latin face has no Arabic letters, so it is never taken for an Arabic interface.
assert!(find_ui_font(std::slice::from_ref(&dir), &names, &['ع'], Region::Other).is_none());
assert!(!arabic_candidates().is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
+17
View File
@@ -58,6 +58,23 @@ mod tests {
assert!(t.rows.iter().all(|r| r.len() <= 64), "a row grew past the column cap");
}
#[test]
fn insert_covered_skips_full_rows_without_walking_them() {
// 50,000 spanning cells over a 50,000-cell row: each walked the row below to its grid
// column before finding it full (billions of steps). A full row is now skipped at once.
let n = 50_000;
let mut t = FTable {
rows: vec![vec![Cell { rowspan: 2, ..Default::default() }; n], vec![Cell::default(); n]],
widths: Vec::new(),
borderless: false,
};
let start = std::time::Instant::now();
t.insert_covered();
assert!(start.elapsed() < std::time::Duration::from_secs(10), "{:?}", start.elapsed());
assert_eq!(t.rows[1].len(), n, "a full row takes no covered cell");
assert!(t.rows[1].iter().all(|c| !c.covered));
}
#[test]
fn base64_round_trip() {
for data in [&b""[..], b"f", b"fo", b"foo", b"foob", b"fooba", b"foobar", &[0, 255, 128, 7]] {
+6 -3
View File
@@ -418,6 +418,11 @@ impl FTable {
return;
}
let Some(row) = self.rows.get_mut(r + k) else { break };
// A full row takes no covered cell: checked before it is walked, so a
// wide row isn't scanned for every span above it.
if row.len() >= 64 {
continue;
}
// Find the index in that row at grid column `g`.
let mut x = 0usize;
let mut at = row.len();
@@ -428,9 +433,7 @@ impl FTable {
}
x += cc.colspan.clamp(1, 63) as usize;
}
if row.len() < 64 {
row.insert(at, Cell { colspan: span as u32, covered: true, ..Default::default() });
}
row.insert(at, Cell { colspan: span as u32, covered: true, ..Default::default() });
}
}
g += span;
+2 -54
View File
@@ -46,37 +46,8 @@ pub fn paragraph_issues(
}
let text = proof_text(p);
let spans: Vec<(std::ops::Range<usize>, wordcraft_proof::ProofLang)> = runs.iter().map(|r| (r.range.clone(), r.lang)).collect();
let skipped = skipped_ranges(runs);
let issues = wordcraft_proof::check_text_with(&text, &spans, spelling, grammar, list_item);
if skipped.is_empty() {
return issues;
}
issues.into_iter().filter(|i| !overlaps(&skipped, i.start, i.end)).collect()
}
/// The byte ranges of the runs left unchecked, in order, those that overlap merged: their
/// starts and ends both rise, so [`overlaps`] finds an issue's by binary search instead of
/// looking at every run for every issue.
fn skipped_ranges(runs: &[ProofRun]) -> Vec<(usize, usize)> {
let mut ranges: Vec<(usize, usize)> = runs.iter().filter(|r| r.skip).map(|r| (r.range.start, r.range.end)).collect();
ranges.sort_unstable();
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
for (s, e) in ranges {
crate::work::steps(1);
match merged.last_mut() {
Some(last) if s < last.1 => last.1 = last.1.max(e),
_ => merged.push((s, e)),
}
}
merged
}
/// Whether bytes `a..b` overlap one of `skipped` ([`skipped_ranges`]): the first range ending
/// after `a` is the only one that can, as every later one starts no earlier.
fn overlaps(skipped: &[(usize, usize)], a: usize, b: usize) -> bool {
crate::work::steps(1);
let k = skipped.partition_point(|&(_, e)| e <= a);
skipped.get(k).is_some_and(|&(s, _)| s < b)
let skip = |a: usize, b: usize| runs.iter().any(|r| r.skip && r.range.start < b && a < r.range.end);
wordcraft_proof::check_text_with(&text, &spans, spelling, grammar, list_item).into_iter().filter(|i| !skip(i.start, i.end)).collect()
}
/// Whether the paragraph is a list item: its own numbering, or its style's, names a list (not 0).
@@ -130,26 +101,3 @@ pub fn proof_text(p: &wordcraft_doc::Paragraph) -> String {
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn run(range: std::ops::Range<usize>, skip: bool) -> ProofRun {
ProofRun { range, lang: Default::default(), skip }
}
/// The lookup finds what a look at every run finds, overlapping and empty runs included.
#[test]
fn skipped_lookup_matches_a_scan_of_every_run() {
let runs = [run(0..4, false), run(4..9, true), run(5..6, true), run(9..9, true), run(9..12, false), run(12..20, true), run(14..30, true)];
let skipped = skipped_ranges(&runs);
for a in 0..32 {
for b in a..32 {
let scan = runs.iter().any(|r| r.skip && r.range.start < b && a < r.range.end);
assert_eq!(overlaps(&skipped, a, b), scan, "{a}..{b}");
}
}
assert!(skipped_ranges(&[run(0..5, false)]).is_empty());
}
}
-3
View File
@@ -116,9 +116,7 @@ impl WordApp {
fonts_korean: false,
fonts_hant: false,
fonts_system_cjk: false,
fonts_system_arabic: false,
want_system_cjk: false,
want_system_arabic: false,
recover_fonts_for: None,
recent_fonts_for: Vec::new(),
fonts_frames: 0,
@@ -225,7 +223,6 @@ impl WordApp {
crate::language_packs::tick(self);
let lang = i18n::Lang::from_pref(&self.ui.language);
i18n::set_current(lang);
crate::rtl::sync(ctx);
// New documents are written in the interface's language when WordCraft can proof it
// (a Polish interface: Polish spelling, grammar and hyphenation from the first word).
// An English interface writes the region's English (English (UK) on a British system,
+1 -14
View File
@@ -155,15 +155,11 @@ impl WordApp {
/// face has changed since the last install.
pub(super) fn install_ui_fonts(&mut self, ctx: &egui::Context, lang: i18n::Lang) {
let system_cjk = self.fonts_system_cjk || self.want_system_cjk || lang.uses_cjk();
// An Arabic interface, or a screen that shows Arabic names (language picker / packs) in
// another interface language, asks for an installed Arabic font when none is embedded.
let system_arabic = lang.rtl() || self.want_system_arabic;
if !self.styled
|| lang.prefers_hans() != self.fonts_hans
|| lang.prefers_korean() != self.fonts_korean
|| lang.prefers_hant() != self.fonts_hant
|| system_cjk != self.fonts_system_cjk
|| system_arabic != self.fonts_system_arabic
{
// Every window's borrowed faces: the fonts are the app's.
let mut faces = self.canvas.ime_faces.clone();
@@ -174,20 +170,11 @@ impl WordApp {
}
}
}
theme::install_fonts_language_arabic(
ctx,
lang.prefers_hans(),
lang.prefers_korean(),
lang.prefers_hant(),
system_cjk,
system_arabic,
&faces,
);
theme::install_fonts_language_extra(ctx, lang.prefers_hans(), lang.prefers_korean(), lang.prefers_hant(), system_cjk, &faces);
self.fonts_hans = lang.prefers_hans();
self.fonts_korean = lang.prefers_korean();
self.fonts_hant = lang.prefers_hant();
self.fonts_system_cjk = system_cjk;
self.fonts_system_arabic = system_arabic;
// Mod with -, = and 0 are Word shortcuts (optional hyphen, subscript, paragraph spacing);
// egui's keyboard zoom would also scale the whole window on them. Zoom is View › Zoom.
// egui quits on Command-Q, which is Ctrl+Q off the Mac: Word's Reset Paragraph
+1 -1
View File
@@ -10,7 +10,7 @@ impl WordApp {
/// whatever handed it over (the command line, a macOS open event, a drop). Returns the message.
pub fn report_open_failure(&mut self, path: &str, error: &str) -> String {
log::warn!("{path}: {error}");
let msg = crate::i18n::fmt(crate::tl!("Couldn't open {path}: {error}"), &[("path", &crate::i18n::isolate_path(path)), ("error", error)]);
let msg = crate::i18n::fmt(crate::tl!("Couldn't open {path}: {error}"), &[("path", path), ("error", error)]);
self.status(msg.clone());
msg
}
+85 -159
View File
@@ -1,6 +1,6 @@
//! File tab (Backstage): Home/New, Open, Info, Save As, Print, Export, Close, Options, About.
use egui::{Rect, Sense, Stroke, Ui, pos2, vec2};
use egui::{Align2, Rect, Sense, Stroke, Ui, pos2, vec2};
use serde_json::json;
use crate::theme::{Tokens, medium, regular, semibold};
@@ -65,14 +65,12 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
app.ui.backstage_page = "home".into();
}
let mac = app.integrated_titlebar;
crate::rtl::start_panel("backstage_nav")
egui::Panel::left("backstage_nav")
.exact_size(200.0)
// Fixed width, as in Word: without this egui shows a resize handle that does nothing (#502).
.resizable(false)
.frame(egui::Frame::NONE.fill(t.nav).inner_margin(egui::Margin { left: 0, right: 0, top: if mac { 0 } else { 12 }, bottom: 12 }))
.show(ui, |ui| {
// Right to left the navigation is on the right, away from the macOS traffic lights.
let rtl = crate::rtl::on();
let back = if mac {
// macOS (#222): the Backstage covers the title bar, so its top band is the title
// bar: the back button sits beside the traffic lights, centred with them and as far
@@ -82,34 +80,18 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
}
// The chevron's stroke starts at the content edge.
let c = if rtl {
pos2(band.max.x - 26.0, band.center().y)
} else {
back_chevron_c(band.min.x, band.center().y, app.session.view.full_screen, app.mac_titlebar.content_left)
};
let c = back_chevron_c(band.min.x, band.center().y, app.session.view.full_screen, app.mac_titlebar.content_left);
let resp = ui.interact(Rect::from_center_size(c, vec2(22.0, band.height())), ui.scope_id().with("backstage_back"), Sense::click());
icons::paint(
ui.painter(),
Rect::from_center_size(c, vec2(18.0, 18.0)),
if rtl { "chevronRight" } else { "chevronLeft" },
egui::Color32::WHITE,
egui::Color32::WHITE,
);
icons::paint(ui.painter(), Rect::from_center_size(c, vec2(18.0, 18.0)), "chevronLeft", egui::Color32::WHITE, egui::Color32::WHITE);
resp
} else {
// The hit area is scoped to the chevron, not the whole 200px-wide row (#3041):
// the row reads as plain sidebar background next to it, so clicking there must
// not also close the File page, same as the macOS chevron fix (#2972) below.
let (r, _) = ui.allocate_exact_size(vec2(200.0, 40.0), Sense::hover());
let c = pos2(crate::rtl::lead_x(rtl, r, 26.0), r.center().y);
let c = pos2(r.min.x + 26.0, r.center().y);
let resp = ui.interact(Rect::from_center_size(c, vec2(22.0, r.height())), ui.scope_id().with("backstage_back"), Sense::click());
icons::paint(
ui.painter(),
Rect::from_center_size(c, vec2(18.0, 18.0)),
if rtl { "chevronRight" } else { "chevronLeft" },
egui::Color32::WHITE,
egui::Color32::WHITE,
);
icons::paint(ui.painter(), Rect::from_center_size(c, vec2(18.0, 18.0)), "chevronLeft", egui::Color32::WHITE, egui::Color32::WHITE);
resp
};
// Escape in the built-in file browser closes only the browser.
@@ -130,16 +112,10 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
ui.painter().rect_filled(r, 0.0, egui::Color32::from_white_alpha(26));
}
let ink = if on { egui::Color32::WHITE } else { egui::Color32::from_white_alpha(110) };
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(crate::rtl::lead_x(rtl, r, 31.0), r.center().y), vec2(18.0, 18.0)),
icon,
ink,
ink,
);
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 31.0, r.center().y), vec2(18.0, 18.0)), icon, ink, ink);
ui.painter().text(
pos2(crate::rtl::lead_x(rtl, r, 52.0), r.center().y),
crate::rtl::lead_center(rtl),
pos2(r.min.x + 52.0, r.center().y),
Align2::LEFT_CENTER,
tl!(label),
if active { semibold(14.0) } else { medium(14.0) },
ink,
@@ -161,8 +137,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
if resp.hovered() {
ui.painter().rect_filled(r, 0.0, egui::Color32::from_white_alpha(26));
}
let at = pos2(crate::rtl::lead_x(rtl, r, 22.0), r.center().y);
ui.painter().text(at, crate::rtl::lead_center(rtl), tl!(label), regular(13.0), egui::Color32::WHITE);
ui.painter().text(pos2(r.min.x + 22.0, r.center().y), Align2::LEFT_CENTER, tl!(label), regular(13.0), egui::Color32::WHITE);
if resp.clicked() {
if id == "discord" {
let _ = app.run("ui.discord", json!({}));
@@ -176,21 +151,19 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.ribbon).inner_margin(egui::Margin::symmetric(40, 30))).show(ui, |ui| {
// The Print page fills the pane and scrolls its settings and its preview itself (#1156).
if app.ui.backstage_page == "print" {
crate::rtl::root(ui, |ui| export_page(app, ui));
export_page(app, ui);
return;
}
egui::ScrollArea::vertical().show(ui, |ui| {
crate::rtl::root(ui, |ui| match app.ui.backstage_page.as_str() {
"home" => home_page(app, ui),
"new" => new_page(app, ui),
"open" => open_page(app, ui),
"saveAs" => save_as_page(app, ui),
"info" => info_page(app, ui),
"recover" => recovery_page(app, ui),
"print" | "export" => export_page(app, ui),
"options" => crate::options::page(app, ui),
_ => home_page(app, ui),
})
egui::ScrollArea::vertical().show(ui, |ui| match app.ui.backstage_page.as_str() {
"home" => home_page(app, ui),
"new" => new_page(app, ui),
"open" => open_page(app, ui),
"saveAs" => save_as_page(app, ui),
"info" => info_page(app, ui),
"recover" => recovery_page(app, ui),
"print" | "export" => export_page(app, ui),
"options" => crate::options::page(app, ui),
_ => home_page(app, ui),
});
});
}
@@ -214,7 +187,7 @@ fn recovery_page(app: &mut WordApp, ui: &mut Ui) {
ui.label(tl!("No recovery copies."));
}
let busy = app.session.recovery_busy();
crate::rtl::grid(ui, egui::Grid::new("recovery_documents").min_col_width(180.0).spacing(vec2(24.0, 16.0)), |ui| {
egui::Grid::new("recovery_documents").min_col_width(180.0).spacing(vec2(24.0, 16.0)).show(ui, |ui| {
if !documents.is_empty() {
ui.strong(tl!("Document"));
ui.strong(tl!("Recovery copy"));
@@ -296,7 +269,7 @@ fn heading(ui: &mut Ui, s: &str) {
/// region-aware page preview, and creates a document from the template on click.
fn template_tile(ui: &mut Ui, app: &mut WordApp, label: &str, template: &str) {
let t = Tokens::get(ui.ctx());
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
// The thumbnail and its caption are one button named by the caption, so a screen reader
// announces "Blank document, button" rather than unnamed text (#3176); the caption opens
// the template too, as in Word.
@@ -540,7 +513,7 @@ fn matching_personal(query: &str, list: &[PersonalTemplate]) -> Vec<PersonalTemp
/// click makes a new document from it.
fn personal_tile(ui: &mut Ui, app: &mut WordApp, p: &PersonalTemplate) {
let t = Tokens::get(ui.ctx());
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing.y = 0.0;
ui.set_max_width(150.0);
let (r, resp) = ui.allocate_exact_size(vec2(150.0, 194.0), Sense::click());
@@ -595,27 +568,15 @@ fn open_list(app: &mut WordApp, ui: &mut Ui) {
let names = shown.iter().map(|p| file_name(p)).collect::<Vec<_>>();
for (p, name) in shown.iter().zip(&names) {
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width().min(700.0), 40.0), Sense::click());
// The row shows a name and a path that may not fit: the path in full is its tooltip.
let resp = resp.on_hover_text(crate::i18n::isolate_path(p));
// Two documents with the same file name are told apart by their path, as on screen.
let spoken = if names.iter().filter(|n| *n == name).count() > 1 { format!("{name}, {p}") } else { name.clone() };
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), &spoken));
if resp.hovered() {
ui.painter().rect_filled(r, 4.0, t.hover);
}
// Names and paths are set apart (a right-to-left line would scramble their marks and
// folders) and the row starts at the side reading starts from.
let rtl = crate::rtl::on();
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(crate::rtl::lead_x(rtl, r, 18.0), r.center().y), vec2(22.0, 22.0)),
"printLayout",
t.icon,
t.accent,
);
let side = crate::rtl::lead_center(rtl);
ui.painter().text(pos2(crate::rtl::lead_x(rtl, r, 40.0), r.min.y + 13.0), side, crate::i18n::isolate(name), medium(13.0), t.text);
ui.painter().text(pos2(crate::rtl::lead_x(rtl, r, 40.0), r.min.y + 29.0), side, crate::i18n::isolate_path(p), regular(11.0), t.text_dim);
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 18.0, r.center().y), vec2(22.0, 22.0)), "printLayout", t.icon, t.accent);
ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 13.0), Align2::LEFT_CENTER, name, medium(13.0), t.text);
ui.painter().text(pos2(r.min.x + 40.0, r.min.y + 29.0), Align2::LEFT_CENTER, p, regular(11.0), t.text_dim);
if resp.clicked() {
let failed = open_recent(app, p).err();
ui.ctx().data_mut(|d| d.insert_temp(open_error_id(), failed.unwrap_or_default()));
@@ -703,108 +664,73 @@ fn save_as_page(app: &mut WordApp, ui: &mut Ui) {
fn info_page(app: &mut WordApp, ui: &mut Ui) {
heading(ui, "Info");
let info = app.session.run("file.info", &json!({})).unwrap_or_default();
// Properties at the reading start, so at the right in a mirrored interface, Statistics after.
let (first, second) = if crate::rtl::on() { (1, 0) } else { (0, 1) };
ui.columns(2, |cols| {
crate::rtl::root(&mut cols[first], |ui| info_properties(app, ui, &info));
crate::rtl::root(&mut cols[second], |ui| info_statistics(ui, &info));
});
}
fn info_properties(app: &mut WordApp, ui: &mut Ui, info: &serde_json::Value) {
ui.label(egui::RichText::new(tl!("Properties")).font(semibold(15.0)));
// The fields edit a copy of the document's properties that is read afresh every frame,
// so each change goes into the document as it is typed (#260): waiting for the field to
// lose focus dropped the keystrokes, and leaving File with Escape never loses it at all.
// The keystrokes of one visit to a field are a single undo step.
let mut props = app.session.doc.core.clone();
let mut edited = None;
let mut focused = None;
crate::rtl::Table::new("props").columns(2).spacing(vec2(12.0, 6.0)).show(ui, |t| {
for (l, v) in [
("Title", &mut props.title),
("Subject", &mut props.subject),
("Author", &mut props.creator),
("Keywords", &mut props.keywords),
("Category", &mut props.category),
] {
t.row(|r| {
let label = r.cell(|ui| ui.label(tl!(l)));
let field = r.cell(|ui| ui.add(egui::TextEdit::singleline(v).id_salt(("info-prop", l))).labelled_by(label.id));
if field.changed() {
let ui = &mut cols[0];
ui.label(egui::RichText::new(tl!("Properties")).font(semibold(15.0)));
// The fields edit a copy of the document's properties that is read afresh every frame,
// so each change goes into the document as it is typed (#260): waiting for the field to
// lose focus dropped the keystrokes, and leaving File with Escape never loses it at all.
// The keystrokes of one visit to a field are a single undo step.
let mut props = app.session.doc.core.clone();
let mut edited = None;
let mut focused = None;
egui::Grid::new("props").num_columns(2).spacing(vec2(12.0, 6.0)).show(ui, |ui| {
for (l, v) in [("Title", &mut props.title), ("Subject", &mut props.subject), ("Author", &mut props.creator), ("Keywords", &mut props.keywords), ("Category", &mut props.category)] {
let label = ui.label(tl!(l));
let r = ui.add(egui::TextEdit::singleline(v).id_salt(("info-prop", l))).labelled_by(label.id);
if r.changed() {
edited = Some(l);
}
if field.has_focus() {
if r.has_focus() {
focused = Some(l);
}
});
ui.end_row();
}
});
if let Some(field) = edited {
if app.info_editing == Some((field, app.session.rev())) {
app.session.join_next_undo();
}
let ok = app.run("file.properties", json!({"title": props.title, "subject": props.subject, "author": props.creator, "keywords": props.keywords, "category": props.category})).is_ok();
app.info_editing = ok.then(|| (field, app.session.rev()));
}
});
if let Some(field) = edited {
if app.info_editing == Some((field, app.session.rev())) {
app.session.join_next_undo();
if focused != app.info_editing.map(|(f, _)| f) {
app.info_editing = None;
}
let ok = app.run("file.properties", json!({"title": props.title, "subject": props.subject, "author": props.creator, "keywords": props.keywords, "category": props.category})).is_ok();
app.info_editing = ok.then(|| (field, app.session.rev()));
}
if focused != app.info_editing.map(|(f, _)| f) {
app.info_editing = None;
}
// Advanced Properties › Custom (#2982): view and edit the document's custom properties.
ui.add_space(6.0);
if ui
.button(egui::RichText::new(tl!("Advanced Properties…")).font(medium(13.5)))
.on_hover_text(tl!("View and edit the document's custom properties."))
.clicked()
{
let _ = app.run("ui.dialog", json!({"name": "customProperties"}));
}
// Protect Document › Encrypt with Password (#55).
ui.add_space(18.0);
ui.label(egui::RichText::new(tl!("Protect Document")).font(semibold(15.0)));
let encrypted = info.get("encrypted").and_then(|v| v.as_bool()).unwrap_or(false);
ui.label(if encrypted {
tl!("A password is required to open this document.")
} else {
tl!("Anyone can open this document. Encrypt it with a password to keep it private.")
});
ui.horizontal(|ui| {
let label = if encrypted { tl!("Change Password…") } else { tl!("Encrypt with Password…") };
if ui.button(egui::RichText::new(label).font(medium(13.5))).clicked() {
let _ = app.run("file.encrypt", json!({}));
// Advanced Properties › Custom (#2982): view and edit the document's custom properties.
ui.add_space(6.0);
if ui
.button(egui::RichText::new(tl!("Advanced Properties…")).font(medium(13.5)))
.on_hover_text(tl!("View and edit the document's custom properties."))
.clicked()
{
let _ = app.run("ui.dialog", json!({"name": "customProperties"}));
}
if encrypted && ui.button(egui::RichText::new(tl!("Remove Password")).font(medium(13.5))).clicked() {
let _ = app.run("file.encrypt", json!({"password": null}));
}
});
}
fn info_statistics(ui: &mut Ui, info: &serde_json::Value) {
ui.label(egui::RichText::new(tl!("Statistics")).font(semibold(15.0)));
for (l, k) in [("Pages", "pages"), ("Words", "words"), ("Paragraphs", "paragraphs"), ("Sections", "sections"), ("Comments", "comments")] {
ui.label(format!("{}: {}", tl!(l), info.get(k).map(|v| v.to_string()).unwrap_or_default()));
}
let path = info.get("path").and_then(|v| v.as_str()).unwrap_or(tl!("Not saved yet"));
// A path reads left to right whatever its folders are called, so it is a label of its own
// beside the words around it (one right-to-left line would swap its folders), and a long
// one is cut short, the whole path on hover, rather than wrapped to strand its last part.
let shown = crate::i18n::isolate_path(path);
let template = tl!("Location: {path}");
let (before, after) = template.split_once("{path}").unwrap_or((template, ""));
// The columns start left to right, so the row's own direction is set: the words before the
// path come first at the right edge, the path after them.
let row = if crate::rtl::on() { egui::Layout::right_to_left(egui::Align::Min) } else { egui::Layout::left_to_right(egui::Align::Min) };
ui.scope_builder(egui::UiBuilder::new().layout(row), |ui| {
if !before.trim().is_empty() {
ui.label(before.trim_end());
}
let font = egui::TextStyle::Body.resolve(ui.style());
let room = ui.available_width() - if after.trim().is_empty() { 0.0 } else { 40.0 };
let fitted = crate::theme::fit_text(ui.painter(), &shown, &font, 0.0, ui.visuals().text_color(), room);
ui.label(fitted).on_hover_text(&shown);
if !after.trim().is_empty() {
ui.label(after.trim_start());
// Protect Document › Encrypt with Password (#55).
ui.add_space(18.0);
ui.label(egui::RichText::new(tl!("Protect Document")).font(semibold(15.0)));
let encrypted = info.get("encrypted").and_then(|v| v.as_bool()).unwrap_or(false);
ui.label(if encrypted {
tl!("A password is required to open this document.")
} else {
tl!("Anyone can open this document. Encrypt it with a password to keep it private.")
});
ui.horizontal(|ui| {
let label = if encrypted { tl!("Change Password…") } else { tl!("Encrypt with Password…") };
if ui.button(egui::RichText::new(label).font(medium(13.5))).clicked() {
let _ = app.run("file.encrypt", json!({}));
}
if encrypted && ui.button(egui::RichText::new(tl!("Remove Password")).font(medium(13.5))).clicked() {
let _ = app.run("file.encrypt", json!({"password": null}));
}
});
let ui = &mut cols[1];
ui.label(egui::RichText::new(tl!("Statistics")).font(semibold(15.0)));
for (l, k) in [("Pages", "pages"), ("Words", "words"), ("Paragraphs", "paragraphs"), ("Sections", "sections"), ("Comments", "comments")] {
ui.label(format!("{}: {}", tl!(l), info.get(k).map(|v| v.to_string()).unwrap_or_default()));
}
let path = info.get("path").and_then(|v| v.as_str()).unwrap_or(tl!("Not saved yet"));
ui.label(crate::i18n::fmt(tl!("Location: {path}"), &[("path", path)]));
});
}
+1 -1
View File
@@ -1199,7 +1199,7 @@ fn finish_canvas_frame(finish: CanvasFinish<'_>, out: egui::scroll_area::ScrollA
if open {
app.escape_free = false;
}
let menu = crate::rtl::menu(&resp).open_bool(&mut shown);
let menu = egui::Popup::menu(&resp).open_bool(&mut shown);
let menu = menu.at_position(app.canvas.context_menu_at.unwrap_or(out.inner_rect.left_top()));
let with_toolbar = crate::mini_toolbar::context_menu_visible(app);
let menu = if with_toolbar { menu.frame(egui::Frame::NONE).close_behavior(egui::PopupCloseBehavior::IgnoreClicks) } else { menu };
+18 -24
View File
@@ -56,7 +56,7 @@ pub(super) fn context_menu(app: &mut WordApp, ui: &mut Ui) {
let item = |ui: &mut Ui, app: &mut WordApp, label: &str, id: &str, params: serde_json::Value| {
let sc = crate::widgets::shortcut_text(app, id);
let on = crate::widgets::enabled(app, id);
if ui.add_enabled(on, crate::rtl::menu_item(ui, tl!(label), &sc, 0.0)).clicked() {
if ui.add_enabled(on, egui::Button::new(tl!(label)).shortcut_text(sc)).clicked() {
let _ = app.run(id, params);
ui.close();
}
@@ -95,19 +95,17 @@ pub(super) fn context_menu(app: &mut WordApp, ui: &mut Ui) {
ui.separator();
if let Some(syn) = app.canvas.context_synonyms.clone().and_then(|v| v.as_array().cloned()).filter(|a| !a.is_empty()) {
ui.menu_button(tl!("Synonyms"), |ui| {
crate::rtl::root(ui, |ui| {
for w in syn.iter().filter_map(|x| x.as_str()) {
if ui.button(w).clicked() {
let _ = app.run("select.word", json!({}));
let (a, b) = app.session.sel.ordered();
let trimmed = app.session.doc.para_at(&a).and_then(|p| p.text.get(a.off..b.off)).map(|t| t.trim_end().len()).unwrap_or(0);
let end = wordcraft_doc::Pos { off: a.off + trimmed, ..b };
app.session.sel = wordcraft_engine::Selection { anchor: a, focus: end };
let _ = app.run("text.insert", json!({"text": w, "raw": true}));
ui.close();
}
for w in syn.iter().filter_map(|x| x.as_str()) {
if ui.button(w).clicked() {
let _ = app.run("select.word", json!({}));
let (a, b) = app.session.sel.ordered();
let trimmed = app.session.doc.para_at(&a).and_then(|p| p.text.get(a.off..b.off)).map(|t| t.trim_end().len()).unwrap_or(0);
let end = wordcraft_doc::Pos { off: a.off + trimmed, ..b };
app.session.sel = wordcraft_engine::Selection { anchor: a, focus: end };
let _ = app.run("text.insert", json!({"text": w, "raw": true}));
ui.close();
}
})
}
});
}
item(ui, app, "Font…", "ui.dialog", json!({"name": "font"}));
@@ -156,12 +154,10 @@ pub(super) fn context_menu(app: &mut WordApp, ui: &mut Ui) {
let pick = table_pick(&app.session);
ui.separator();
ui.menu_button(tl!("Insert"), |ui| {
crate::rtl::root(ui, |ui| {
item(ui, app, "Insert Columns to the Left", "table.insertColumnLeft", json!({}));
item(ui, app, "Insert Columns to the Right", "table.insertColumnRight", json!({}));
item(ui, app, "Insert Rows Above", "table.insertRowAbove", json!({}));
item(ui, app, "Insert Rows Below", "table.insertRowBelow", json!({}));
})
item(ui, app, "Insert Columns to the Left", "table.insertColumnLeft", json!({}));
item(ui, app, "Insert Columns to the Right", "table.insertColumnRight", json!({}));
item(ui, app, "Insert Rows Above", "table.insertRowAbove", json!({}));
item(ui, app, "Insert Rows Below", "table.insertRowBelow", json!({}));
});
// Every Delete is there whatever is selected, as on Word's Delete button: with the caret
// in a cell, Delete Table is how the table goes (#1198).
@@ -183,11 +179,9 @@ pub(super) fn context_menu(app: &mut WordApp, ui: &mut Ui) {
}
if pick == TablePick::Table {
ui.menu_button(tl!("AutoFit"), |ui| {
crate::rtl::root(ui, |ui| {
item(ui, app, "AutoFit Contents", "table.autofit", json!({"mode": "contents"}));
item(ui, app, "AutoFit Window", "table.autofit", json!({"mode": "window"}));
item(ui, app, "Fixed Column Width", "table.autofit", json!({"mode": "fixed"}));
})
item(ui, app, "AutoFit Contents", "table.autofit", json!({"mode": "contents"}));
item(ui, app, "AutoFit Window", "table.autofit", json!({"mode": "window"}));
item(ui, app, "Fixed Column Width", "table.autofit", json!({"mode": "fixed"}));
});
}
item(ui, app, "Text Direction", "table.textDirection", json!({}));
+250 -293
View File
@@ -12,7 +12,7 @@ pub(crate) fn dictionary_problem(lang: &str, unreadable: &Option<(std::path::Pat
match (unreadable, missing) {
(Some((file, reason)), _) => Some(crate::i18n::fmt(
tl!("The spelling dictionary for {language} couldn't be read, so it isn't checked. File: {file}. Reason: {reason}"),
&[("language", lang), ("file", &crate::i18n::isolate_path(&file.to_string_lossy())), ("reason", reason)],
&[("language", lang), ("file", &file.to_string_lossy()), ("reason", reason)],
)),
(None, Some(name)) => Some(crate::i18n::fmt(
tl!(
@@ -93,9 +93,9 @@ fn quick_access(ui: &mut Ui, app: &mut WordApp) {
}
icons::paint(ui.painter(), Rect::from_center_size(r.center(), vec2(16.0, 16.0)), "more", t.icon, t.accent);
let resp = resp.on_hover_text(tl!("Customize Quick Access Toolbar"));
crate::rtl::menu(&resp).show(|ui| {
egui::Popup::menu(&resp).show(|ui| {
ui.set_min_width(220.0);
crate::rtl::root(ui, |ui| qat_menu(ui, app));
qat_menu(ui, app);
});
}
@@ -144,7 +144,7 @@ fn undo_list_button(ui: &mut Ui, app: &mut WordApp, enabled: bool) {
let c = if enabled { t.icon } else { t.text_disabled };
icons::paint(ui.painter(), Rect::from_center_size(r.center(), vec2(9.0, 9.0)), "dropdown", c, c);
let resp = resp.on_hover_text(tl!("Undo History"));
crate::rtl::menu(&resp).show(|ui| undo_list(ui, app));
egui::Popup::menu(&resp).show(|ui| undo_list(ui, app));
}
/// The Undo list (#701): recent steps, newest first. Hovering a row highlights it and every newer
@@ -275,12 +275,6 @@ impl MacTitleBar {
}
}
/// Where the AutoSave switch's knob sits in its track `r`: on at the right and off at the left,
/// mirrored in a right-to-left interface (on at the left).
fn autosave_knob_x(rtl: bool, on: bool, r: Rect) -> f32 {
crate::rtl::lead_x(rtl != on, r, 8.0)
}
/// Draws the title bar with window dragging, AutoSave, quick access, account, community and search
/// controls, and the centred clickable document title.
pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
@@ -305,97 +299,83 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max));
}
// The free stretch between the two clusters, for the centred title. Right to left the
// clusters swap sides: AutoSave and the Quick Access Toolbar start at the right.
let rtl = crate::rtl::on();
let mut qat_end = if rtl { full.max.x } else { full.min.x };
let mut right_start = if rtl { full.min.x } else { full.max.x };
crate::rtl::root(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing = vec2(2.0, 0.0);
// AutoSave switch: greyed out, with the reason as its tooltip, when AutoSave can't
// cover the document (#196, #176); clicking it then offers Save As where that helps.
let block = app.autosave_block();
let on = app.autosaves();
let color = if block.is_some() { t.text_dim.gamma_multiply(0.6) } else { t.text_dim };
// The text is part of the switch (#3370): clicking it toggles AutoSave, and chrome
// text is not document text to select (double-clicking it copied "AutoSave").
let text = ui.add(
egui::Label::new(egui::RichText::new(tl!("AutoSave")).font(regular(11.5)).color(color))
.selectable(false)
.sense(Sense::click()),
);
let (r, resp) = ui.allocate_exact_size(vec2(30.0, 16.0), Sense::click());
let (fill, stroke, knob_fill) = match (&block, on) {
(Some(_), _) => (t.input.gamma_multiply(0.5), t.border.gamma_multiply(0.7), t.text_dim.gamma_multiply(0.45)),
(None, true) => (t.accent, t.accent, t.on_accent),
(None, false) => (t.input, t.border_strong, t.text_dim),
};
ui.painter().rect(r, 8.0, fill, Stroke::new(1.0, stroke), egui::StrokeKind::Inside);
let knob = autosave_knob_x(rtl, on, r);
ui.painter().circle_filled(pos2(knob, r.center().y), 5.0, knob_fill);
let tip = block
.as_ref()
.map_or_else(|| tl!("AutoSave saves every change to the file (needs a saved document)").to_string(), |b| b.reason());
let text = text.on_hover_text(tip.as_str()).on_hover_cursor(egui::CursorIcon::PointingHand);
let resp = resp.on_hover_text(tip.as_str()).on_hover_cursor(egui::CursorIcon::PointingHand);
if text.clicked() || resp.clicked() {
match block {
None => {
let _ = app.run("file.autosave", json!({}));
}
// AutoSave comes on once Save As has saved (maybe on a later frame, #94).
Some(b) if b.save_as_helps() => {
let _ = app.save_as(crate::file_dialogs::AfterSave::AutoSave);
}
Some(b) => app.status(b.reason()),
let mut qat_end = full.min.x;
let mut right_start = full.max.x;
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing = vec2(2.0, 0.0);
// AutoSave switch: greyed out, with the reason as its tooltip, when AutoSave can't
// cover the document (#196, #176); clicking it then offers Save As where that helps.
let block = app.autosave_block();
let on = app.autosaves();
let color = if block.is_some() { t.text_dim.gamma_multiply(0.6) } else { t.text_dim };
// The text is part of the switch (#3370): clicking it toggles AutoSave, and chrome
// text is not document text to select (double-clicking it copied "AutoSave").
let text = ui.add(
egui::Label::new(egui::RichText::new(tl!("AutoSave")).font(regular(11.5)).color(color)).selectable(false).sense(Sense::click()),
);
let (r, resp) = ui.allocate_exact_size(vec2(30.0, 16.0), Sense::click());
let (fill, stroke, knob_fill) = match (&block, on) {
(Some(_), _) => (t.input.gamma_multiply(0.5), t.border.gamma_multiply(0.7), t.text_dim.gamma_multiply(0.45)),
(None, true) => (t.accent, t.accent, t.on_accent),
(None, false) => (t.input, t.border_strong, t.text_dim),
};
ui.painter().rect(r, 8.0, fill, Stroke::new(1.0, stroke), egui::StrokeKind::Inside);
let knob = if on { r.max.x - 8.0 } else { r.min.x + 8.0 };
ui.painter().circle_filled(pos2(knob, r.center().y), 5.0, knob_fill);
let tip = block
.as_ref()
.map_or_else(|| tl!("AutoSave saves every change to the file (needs a saved document)").to_string(), |b| b.reason());
let text = text.on_hover_text(tip.as_str()).on_hover_cursor(egui::CursorIcon::PointingHand);
let resp = resp.on_hover_text(tip.as_str()).on_hover_cursor(egui::CursorIcon::PointingHand);
if text.clicked() || resp.clicked() {
match block {
None => {
let _ = app.run("file.autosave", json!({}));
}
// AutoSave comes on once Save As has saved (maybe on a later frame, #94).
Some(b) if b.save_as_helps() => {
let _ = app.save_as(crate::file_dialogs::AfterSave::AutoSave);
}
Some(b) => app.status(b.reason()),
}
}
ui.add_space(8.0);
// Hidden (#1945): neither the buttons nor the … menu; the title centres as before.
if app.ui.quick_access_toolbar {
quick_access(ui, app);
}
qat_end = ui.min_rect().max.x;
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// Account / community.
let (r, resp) = ui.allocate_exact_size(vec2(control_h, control_h), Sense::click());
ui.painter().circle_filled(r.center(), control_h / 2.0 - 1.0, t.accent);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tl!("Account")));
let initials: String = app.session.user_initials().chars().take(2).collect();
ui.painter().text(r.center(), Align2::CENTER_CENTER, initials, semibold(10.5), t.on_accent);
if resp.on_hover_text(format!("{} — set your name in File › Options", app.session.author)).clicked() {
let _ = app.run("ui.backstage", json!({"value": true, "page": "options", "pane": "general"}));
}
ui.add_space(8.0);
// Hidden (#1945): neither the buttons nor the … menu; the title centres as before.
if app.ui.quick_access_toolbar {
quick_access(ui, app);
}
qat_end = if rtl { ui.min_rect().min.x } else { ui.min_rect().max.x };
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
// Account / community.
let (r, resp) = ui.allocate_exact_size(vec2(control_h, control_h), Sense::click());
ui.painter().circle_filled(r.center(), control_h / 2.0 - 1.0, t.accent);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tl!("Account")));
let initials: String = app.session.user_initials().chars().take(2).collect();
ui.painter().text(r.center(), Align2::CENTER_CENTER, initials, semibold(10.5), t.on_accent);
if resp.on_hover_text(format!("{} — set your name in File › Options", app.session.author)).clicked() {
let _ = app.run("ui.backstage", json!({"value": true, "page": "options", "pane": "general"}));
if app.ui.show_discord {
let (r, resp) = ui.allocate_exact_size(vec2(84.0, 22.0), Sense::click());
ui.painter().rect_filled(r, 11.0, if resp.hovered() { t.accent_text } else { t.accent });
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(r.min.x + 13.0, r.center().y), vec2(14.0, 14.0)),
"discord",
t.on_accent,
t.on_accent,
);
ui.painter().text(pos2(r.min.x + 24.0, r.center().y), Align2::LEFT_CENTER, tl!("Discord"), medium(11.5), t.on_accent);
if resp.on_hover_text(tl!("Join the ArtCraft community on Discord")).clicked() {
let _ = app.run("ui.discord", json!({}));
}
ui.add_space(8.0);
if app.ui.show_discord {
let (r, resp) = ui.allocate_exact_size(vec2(84.0, 22.0), Sense::click());
ui.painter().rect_filled(r, 11.0, if resp.hovered() { t.accent_text } else { t.accent });
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(crate::rtl::lead_x(rtl, r, 13.0), r.center().y), vec2(14.0, 14.0)),
"discord",
t.on_accent,
t.on_accent,
);
ui.painter().text(
pos2(crate::rtl::lead_x(rtl, r, 24.0), r.center().y),
crate::rtl::lead_center(rtl),
tl!("Discord"),
medium(11.5),
t.on_accent,
);
if resp.on_hover_text(tl!("Join the ArtCraft community on Discord")).clicked() {
let _ = app.run("ui.discord", json!({}));
}
ui.add_space(8.0);
}
search_box(app, ui, control_h);
right_start = if rtl { ui.min_rect().max.x } else { ui.min_rect().min.x };
});
})
}
search_box(app, ui, control_h);
right_start = ui.min_rect().min.x;
});
});
let (qat_end, right_start) = if rtl { (right_start, qat_end) } else { (qat_end, right_start) };
// Centred title.
let title = if !app.has_document() {
// No document open (#1129): the app's name alone.
@@ -403,7 +383,7 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
} else {
format!(
"{} — {}",
crate::i18n::isolate(&app.title_stem()),
app.title_stem(),
if app.session.dirty {
tl!("Edited")
} else if app.session.path.is_some() {
@@ -450,24 +430,17 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
/// of Full Screen (#1678).
fn search_box(app: &mut WordApp, ui: &mut Ui, control_h: f32) {
let t = Tokens::get(ui.ctx());
let rtl = crate::rtl::on();
let (r, resp) = ui.allocate_exact_size(vec2(260.0, control_h), Sense::click());
let ph = tl!("Search commands and help");
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, ph));
ui.painter().rect(r, 6.0, if resp.hovered() { t.input } else { t.ribbon }, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(crate::rtl::lead_x(rtl, r, 16.0), r.center().y), vec2(16.0, 16.0)),
"search",
t.text_dim,
t.accent,
);
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 16.0, r.center().y), vec2(16.0, 16.0)), "search", t.text_dim, t.accent);
let pfont = TypeRung::Control.regular();
let ptrack = TypeRung::Control.tracking();
paint_text(
ui.painter(),
pos2(crate::rtl::lead_x(rtl, r, 30.0), r.center().y - TypeRung::Control.line_height() / 2.0),
crate::rtl::lead_top(rtl),
pos2(r.min.x + 30.0, r.center().y - TypeRung::Control.line_height() / 2.0),
Align2::LEFT_TOP,
ph,
&pfont,
ptrack,
@@ -582,203 +555,187 @@ pub fn status_bar(app: &mut WordApp, ui: &mut Ui) {
if !app.has_document() {
return;
}
let rtl = crate::rtl::on();
crate::rtl::root(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing = vec2(14.0, 0.0);
let l = app.session.layout();
let page = if let Some(p) = app.canvas.viewed_page {
// The page in view (#3880), as the scrollbar's tip names it.
p.min(l.pages.len().saturating_sub(1)) + 1
} else if app.session.selects_whole_story() {
app.session.page_hint.min(l.pages.len().saturating_sub(1)) + 1
} else {
l.caret_on(&app.session.sel.focus, app.session.page_hint).map(|c| c.page + 1).unwrap_or(1)
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing = vec2(14.0, 0.0);
let l = app.session.layout();
let page = if let Some(p) = app.canvas.viewed_page {
// The page in view (#3880), as the scrollbar's tip names it.
p.min(l.pages.len().saturating_sub(1)) + 1
} else if app.session.selects_whole_story() {
app.session.page_hint.min(l.pages.len().saturating_sub(1)) + 1
} else {
l.caret_on(&app.session.sel.focus, app.session.page_hint).map(|c| c.page + 1).unwrap_or(1)
};
let st = |ui: &mut Ui, s: &str| {
let font = TypeRung::Caption.regular();
let track = TypeRung::Caption.tracking();
let w = text_width(ui.painter(), s, &font, track, t.text_dim);
let (r, resp) = ui.allocate_exact_size(vec2(w + 1.0, TypeRung::Caption.line_height()), Sense::click());
paint_text(ui.painter(), pos2(r.min.x, r.min.y), Align2::LEFT_TOP, s, &font, track, t.text_dim);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, s));
resp
};
if st(ui, &crate::i18n::fmt(tl!("Page {page} of {pages}"), &[("page", &page.to_string()), ("pages", &l.pages.len().to_string())]))
.on_hover_text(tl!("Go To (⌘⌥G)"))
.clicked()
{
let _ = app.run("ui.dialog", json!({"name": "goto"}));
}
let words = app.cached_word_count();
// A selected picture/shape/text box isn't a text selection: show the total.
let wtxt = if app.session.sel.is_collapsed() || crate::objects::selected(app).is_some() {
crate::i18n::fmt(tl!("{words} words"), &[("words", &words.to_string())])
} else {
let sw = wordcraft_doc::count_words(&app.session.selected_text());
crate::i18n::fmt(tl!("{selected} of {words} words"), &[("selected", &sw.to_string()), ("words", &words.to_string())])
};
if st(ui, &wtxt).clicked() {
let _ = app.run("ui.dialog", json!({"name": "wordCount"}));
}
if app.session.sel.focus.story != StoryRef::Body && !crate::canvas::in_text_box(app) {
let label = match crate::canvas::part_kind(app, app.session.sel.focus.story) {
Some(PartKind::Footnote) => tl!("Editing footnote"),
Some(PartKind::Endnote) => tl!("Editing endnote"),
_ => tl!("Editing header/footer"),
};
let st = |ui: &mut Ui, s: &str| {
let font = TypeRung::Caption.regular();
let track = TypeRung::Caption.tracking();
let w = text_width(ui.painter(), s, &font, track, t.text_dim);
let (r, resp) = ui.allocate_exact_size(vec2(w + 1.0, TypeRung::Caption.line_height()), Sense::click());
paint_text(ui.painter(), pos2(r.min.x, r.min.y), Align2::LEFT_TOP, s, &font, track, t.text_dim);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, s));
resp
};
if st(ui, &crate::i18n::fmt(tl!("Page {page} of {pages}"), &[("page", &page.to_string()), ("pages", &l.pages.len().to_string())]))
.on_hover_text(tl!("Go To (⌘⌥G)"))
.clicked()
{
let _ = app.run("ui.dialog", json!({"name": "goto"}));
st(ui, label);
}
// The language at the caret; a click sets the proofing language (Review › Language).
let lang = crate::ribbon::caret_language(app);
let missing = crate::ribbon::caret_missing_dictionary(app);
let unreadable = crate::ribbon::caret_unreadable_dictionary(app);
let shown = if unreadable.is_some() {
crate::i18n::fmt(tl!("{language} (dictionary unreadable)"), &[("language", &lang)])
} else if missing.is_some() {
crate::i18n::fmt(tl!("{language} (no dictionary)"), &[("language", &lang)])
} else {
lang.clone()
};
let tip = dictionary_problem(&lang, &unreadable, missing).unwrap_or_else(|| tl!("Language").to_string());
let resp = st(ui, &shown).on_hover_text(tip);
egui::Popup::menu(&resp).show(|ui| {
ui.set_min_width(200.0);
crate::ribbon::language_menu(ui, app);
});
if app.session.doc.settings.track_changes {
st(ui, tl!("Track Changes: On"));
}
if app.session.prefs.overtype && st(ui, tl!("Overtype")).on_hover_text(tl!("Click to switch back to inserting")).clicked() {
let _ = app.run("text.overtype", json!({"value": false}));
}
// The message is drawn where the row is now; the controls on the right are laid
// out first, so the message can be shortened to the space they leave (#2402): a
// long one (the path of a file that isn't there) must not be drawn over the
// Focus and view buttons.
let msg_at = ui.cursor().min;
let mut controls_left = f32::INFINITY;
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.spacing_mut().item_spacing = vec2(4.0, 0.0);
let reading = app.session.view.read_mode;
let z = (if reading { app.session.view.read_zoom } else { app.canvas.scale / crate::canvas::PX_PER_PT } * 100.0).round();
let ztxt = format!("{z}%");
let zfont = TypeRung::Caption.regular();
let ztrack = TypeRung::Caption.tracking();
let zw = text_width(ui.painter(), &ztxt, &zfont, ztrack, t.text_dim);
// A fixed width for three digits plus a small reserve, so the controls to the
// left don't shift as the percentage gains or loses digits (#1975).
let zslot = text_width(ui.painter(), "000%", &zfont, ztrack, t.text_dim) + 4.0;
let (zr, zresp) = ui.allocate_exact_size(vec2(zw.max(zslot) + 1.0, TypeRung::Caption.line_height()), Sense::click());
paint_text(ui.painter(), pos2(zr.max.x - 1.0, zr.min.y), Align2::RIGHT_TOP, &ztxt, &zfont, ztrack, t.text_dim);
if zresp.clicked() && !reading {
let _ = app.run("ui.dialog", json!({ "name": "zoom" }));
}
let words = app.cached_word_count();
// A selected picture/shape/text box isn't a text selection: show the total.
let wtxt = if app.session.sel.is_collapsed() || crate::objects::selected(app).is_some() {
crate::i18n::fmt(tl!("{words} words"), &[("words", &words.to_string())])
} else {
let sw = wordcraft_doc::count_words(&app.session.selected_text());
crate::i18n::fmt(tl!("{selected} of {words} words"), &[("selected", &sw.to_string()), ("words", &words.to_string())])
};
if st(ui, &wtxt).clicked() {
let _ = app.run("ui.dialog", json!({"name": "wordCount"}));
if small_icon(ui, "plus", tl!("Zoom In")) {
if reading {
let _ = app.run("view.readTextSize", json!({"value": (z + 10.0).min(150.0)}));
} else {
let _ = app.run("view.zoomIn", json!({}));
}
}
if app.session.sel.focus.story != StoryRef::Body && !crate::canvas::in_text_box(app) {
let label = match crate::canvas::part_kind(app, app.session.sel.focus.story) {
Some(PartKind::Footnote) => tl!("Editing footnote"),
Some(PartKind::Endnote) => tl!("Editing endnote"),
_ => tl!("Editing header/footer"),
};
st(ui, label);
}
// The language at the caret; a click sets the proofing language (Review › Language).
let lang = crate::ribbon::caret_language(app);
let missing = crate::ribbon::caret_missing_dictionary(app);
let unreadable = crate::ribbon::caret_unreadable_dictionary(app);
let shown = if unreadable.is_some() {
crate::i18n::fmt(tl!("{language} (dictionary unreadable)"), &[("language", &lang)])
} else if missing.is_some() {
crate::i18n::fmt(tl!("{language} (no dictionary)"), &[("language", &lang)])
} else {
lang.clone()
};
let tip = dictionary_problem(&lang, &unreadable, missing).unwrap_or_else(|| tl!("Language").to_string());
let resp = st(ui, &shown).on_hover_text(tip);
crate::rtl::menu(&resp).show(|ui| {
ui.set_min_width(200.0);
crate::rtl::root(ui, |ui| crate::ribbon::language_menu(ui, app));
// Reading zoom changes only reading presentation, never the normal view zoom.
let mut v = if reading { app.session.view.read_zoom } else { app.canvas.scale / crate::canvas::PX_PER_PT };
let slider = egui::Slider::new(&mut v, if reading { 0.75..=1.5 } else { 0.1..=5.0 }).logarithmic(!reading).show_value(false);
ui.spacing_mut().slider_width = 110.0;
let response = ui.add(slider);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::Role::Slider, ui.is_enabled(), if reading { tl!("Text Size") } else { tl!("Zoom") })
});
if app.session.doc.settings.track_changes {
st(ui, tl!("Track Changes: On"));
if response.changed() {
let command = if reading { "view.readTextSize" } else { "view.zoom" };
let _ = app.run(command, json!({"value": (v * 100.0).round()}));
}
if app.session.prefs.overtype && st(ui, tl!("Overtype")).on_hover_text(tl!("Click to switch back to inserting")).clicked() {
let _ = app.run("text.overtype", json!({"value": false}));
if small_icon(ui, "minus", tl!("Zoom Out")) {
if reading {
let _ = app.run("view.readTextSize", json!({"value": (z - 10.0).max(75.0)}));
} else {
let _ = app.run("view.zoomOut", json!({}));
}
}
// The message is drawn where the row is now; the controls on the right are laid
// out first, so the message can be shortened to the space they leave (#2402): a
// long one (the path of a file that isn't there) must not be drawn over the
// Focus and view buttons.
// Right to left the row runs the other way: the message starts at the right edge of
// what is left, and the controls (a cluster at the left) end at their right edge.
let msg_at = ui.cursor().min;
let msg_edge = ui.available_rect_before_wrap().max.x;
let mut controls_left = if rtl { f32::NEG_INFINITY } else { f32::INFINITY };
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.spacing_mut().item_spacing = vec2(4.0, 0.0);
let reading = app.session.view.read_mode;
let z = (if reading { app.session.view.read_zoom } else { app.canvas.scale / crate::canvas::PX_PER_PT } * 100.0).round();
let ztxt = format!("{z}%");
let zfont = TypeRung::Caption.regular();
let ztrack = TypeRung::Caption.tracking();
let zw = text_width(ui.painter(), &ztxt, &zfont, ztrack, t.text_dim);
// A fixed width for three digits plus a small reserve, so the controls to the
// left don't shift as the percentage gains or loses digits (#1975).
let zslot = text_width(ui.painter(), "000%", &zfont, ztrack, t.text_dim) + 4.0;
let (zr, zresp) = ui.allocate_exact_size(vec2(zw.max(zslot) + 1.0, TypeRung::Caption.line_height()), Sense::click());
paint_text(ui.painter(), pos2(zr.max.x - 1.0, zr.min.y), Align2::RIGHT_TOP, &ztxt, &zfont, ztrack, t.text_dim);
if zresp.clicked() && !reading {
let _ = app.run("ui.dialog", json!({ "name": "zoom" }));
}
// The slider grows to the right in every language, so the buttons stay at its
// ends: plus on the right. This cluster fills from the far end inwards.
let zoom_step = |app: &mut WordApp, plus: bool| {
if reading {
let value = if plus { (z + 10.0).min(150.0) } else { (z - 10.0).max(75.0) };
let _ = app.run("view.readTextSize", json!({"value": value}));
} else {
let _ = app.run(if plus { "view.zoomIn" } else { "view.zoomOut" }, json!({}));
}
};
let (inner, outer) = (("minus", "Zoom Out", false), ("plus", "Zoom In", true));
let (near, far) = if rtl { (inner, outer) } else { (outer, inner) };
if small_icon(ui, near.0, tl!(near.1)) {
zoom_step(app, near.2);
}
// Reading zoom changes only reading presentation, never the normal view zoom.
let mut v = if reading { app.session.view.read_zoom } else { app.canvas.scale / crate::canvas::PX_PER_PT };
let slider = egui::Slider::new(&mut v, if reading { 0.75..=1.5 } else { 0.1..=5.0 }).logarithmic(!reading).show_value(false);
ui.spacing_mut().slider_width = 110.0;
let response = ui.add(slider);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::Role::Slider, ui.is_enabled(), if reading { tl!("Text Size") } else { tl!("Zoom") })
});
if response.changed() {
let command = if reading { "view.readTextSize" } else { "view.zoom" };
let _ = app.run(command, json!({"value": (v * 100.0).round()}));
}
if small_icon(ui, far.0, tl!(far.1)) {
zoom_step(app, far.2);
}
ui.add_space(10.0);
let mode = app.session.view.mode;
let read = app.session.view.read_mode;
for (icon, id, name, on) in [
("webLayout", "view.webLayout", tl!("Web Layout"), mode == wordcraft_layout::ViewMode::Web),
("printLayout", "view.printLayout", tl!("Print Layout"), mode == wordcraft_layout::ViewMode::Print && !read),
("readMode", "view.readMode", tl!("Read Mode"), read),
] {
let (r, resp) = ui.allocate_exact_size(vec2(22.0, 20.0), Sense::click());
if on {
ui.painter().rect_filled(r, 3.0, t.checked);
} else if resp.hovered() {
ui.painter().rect_filled(r, 3.0, t.hover);
}
icons::paint(ui.painter(), r.shrink(3.0), icon, t.icon, t.accent);
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, on, name));
if resp.on_hover_text(name).clicked() {
let _ = app.run(id, json!({}));
}
}
ui.add_space(8.0);
let focus = tl!("Focus");
let focus_w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(focus.to_string(), regular(11.5), t.text_dim).size().x);
let (r, resp) = ui.allocate_exact_size(vec2((focus_w + 26.0).max(56.0), 20.0), Sense::click());
// The Focus button is the last of the controls, so it is the leftmost one: the
// message has to stop before it (#2402).
controls_left = if rtl { r.max.x } else { r.min.x };
if resp.hovered() {
ui.add_space(10.0);
let mode = app.session.view.mode;
let read = app.session.view.read_mode;
for (icon, id, name, on) in [
("webLayout", "view.webLayout", tl!("Web Layout"), mode == wordcraft_layout::ViewMode::Web),
("printLayout", "view.printLayout", tl!("Print Layout"), mode == wordcraft_layout::ViewMode::Print && !read),
("readMode", "view.readMode", tl!("Read Mode"), read),
] {
let (r, resp) = ui.allocate_exact_size(vec2(22.0, 20.0), Sense::click());
if on {
ui.painter().rect_filled(r, 3.0, t.checked);
} else if resp.hovered() {
ui.painter().rect_filled(r, 3.0, t.hover);
}
let icon_at = pos2(crate::rtl::lead_left(rtl, r, 2.0, 16.0), r.min.y + 2.0);
icons::paint(ui.painter(), Rect::from_min_size(icon_at, vec2(16.0, 16.0)), "focus", t.icon, t.accent);
let ffont = TypeRung::Caption.regular();
let ftrack = TypeRung::Caption.tracking();
paint_text(
ui.painter(),
pos2(crate::rtl::lead_x(rtl, r, 22.0), r.center().y - TypeRung::Caption.line_height() / 2.0),
crate::rtl::lead_top(rtl),
focus,
&ffont,
ftrack,
t.text_dim,
);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, focus));
if resp.clicked() {
let _ = app.run("view.focus", json!({}));
}
});
if let Some((msg, at)) = &app.status_msg
&& crate::now_ms() - at < 6000.0
{
let font = TypeRung::Caption.regular();
let track = TypeRung::Caption.tracking();
// The room the message has: what is left between here and the controls.
let room = if rtl {
msg_edge - controls_left - ui.spacing().item_spacing.x
} else {
controls_left - ui.spacing().item_spacing.x - msg_at.x
};
let msg_shown = fit_text(ui.painter(), msg, &font, track, t.accent_text, room);
if !msg_shown.is_empty() {
let w = text_width(ui.painter(), &msg_shown, &font, track, t.accent_text);
let at = if rtl { pos2(msg_edge - w - 1.0, msg_at.y) } else { msg_at };
let rect = Rect::from_min_size(at, vec2(w + 1.0, TypeRung::Caption.line_height()));
// The whole message is in the tooltip; the bar only has room for the
// beginning of it (#2402).
let _ = ui.interact(rect, status_message_id(), Sense::hover()).on_hover_text(msg.clone());
paint_text(ui.painter(), rect.min, Align2::LEFT_TOP, &msg_shown, &font, track, t.accent_text);
ui.allocate_space(vec2(w + 1.0, TypeRung::Caption.line_height()));
icons::paint(ui.painter(), r.shrink(3.0), icon, t.icon, t.accent);
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, on, name));
if resp.on_hover_text(name).clicked() {
let _ = app.run(id, json!({}));
}
}
})
ui.add_space(8.0);
let focus = tl!("Focus");
let focus_w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(focus.to_string(), regular(11.5), t.text_dim).size().x);
let (r, resp) = ui.allocate_exact_size(vec2((focus_w + 26.0).max(56.0), 20.0), Sense::click());
// The Focus button is the last of the controls, so it is the leftmost one: the
// message has to stop before it (#2402).
controls_left = r.min.x;
if resp.hovered() {
ui.painter().rect_filled(r, 3.0, t.hover);
}
icons::paint(ui.painter(), Rect::from_min_size(r.min + vec2(2.0, 2.0), vec2(16.0, 16.0)), "focus", t.icon, t.accent);
let ffont = TypeRung::Caption.regular();
let ftrack = TypeRung::Caption.tracking();
paint_text(
ui.painter(),
pos2(r.min.x + 22.0, r.center().y - TypeRung::Caption.line_height() / 2.0),
Align2::LEFT_TOP,
focus,
&ffont,
ftrack,
t.text_dim,
);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, focus));
if resp.clicked() {
let _ = app.run("view.focus", json!({}));
}
});
if let Some((msg, at)) = &app.status_msg
&& crate::now_ms() - at < 6000.0
{
let font = TypeRung::Caption.regular();
let track = TypeRung::Caption.tracking();
// The room the message has: what is left between here and the controls.
let room = controls_left - ui.spacing().item_spacing.x - msg_at.x;
let msg_shown = fit_text(ui.painter(), msg, &font, track, t.accent_text, room);
if !msg_shown.is_empty() {
let w = text_width(ui.painter(), &msg_shown, &font, track, t.accent_text);
let rect = Rect::from_min_size(msg_at, vec2(w + 1.0, TypeRung::Caption.line_height()));
// The whole message is in the tooltip; the bar only has room for the
// beginning of it (#2402).
let _ = ui.interact(rect, status_message_id(), Sense::hover()).on_hover_text(msg.clone());
paint_text(ui.painter(), rect.min, Align2::LEFT_TOP, &msg_shown, &font, track, t.accent_text);
ui.allocate_space(vec2(w + 1.0, TypeRung::Caption.line_height()));
}
}
});
});
}
-11
View File
@@ -307,14 +307,3 @@ fn clicking_the_autosave_label_toggles_autosave() {
assert!(h.state().autosaves(), "and clicking it again turns it back on");
let _ = std::fs::remove_dir_all(dir);
}
/// The AutoSave knob is at the right when the switch is on and at the left when it is off; a
/// right-to-left interface mirrors that.
#[test]
fn the_autosave_knob_sits_right_when_on_and_mirrors_right_to_left() {
let track = Rect::from_min_max(egui::pos2(100.0, 0.0), egui::pos2(130.0, 16.0));
assert_eq!(autosave_knob_x(false, true, track), 122.0, "left to right, on: right");
assert_eq!(autosave_knob_x(false, false, track), 108.0, "left to right, off: left");
assert_eq!(autosave_knob_x(true, true, track), 108.0, "right to left, on: left");
assert_eq!(autosave_knob_x(true, false, track), 122.0, "right to left, off: right");
}
+4 -4
View File
@@ -497,14 +497,14 @@ fn body(ui: &mut Ui, d: &mut ColorsDialog) -> Option<bool> {
});
ui.separator();
ui.horizontal(|ui| {
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.set_width(330.0);
match d.tab {
ColorsTab::Standard => standard(ui, d),
ColorsTab::Custom => custom(ui, d),
}
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.add_space(110.0);
preview(ui, d, &t);
});
@@ -512,7 +512,7 @@ fn body(ui: &mut Ui, d: &mut ColorsDialog) -> Option<bool> {
let mut out = None;
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (okb, cancelb) = crate::dialogs::ok_cancel_responses(ui, |ui| ui.add(crate::widgets::primary_button(ui, tl!("OK"))), tl!("Cancel"));
if cancelb.clicked() {
out = Some(false);
@@ -670,7 +670,7 @@ fn custom(ui: &mut Ui, d: &mut ColorsDialog) {
ui.add_space(8.0);
});
ui.add_space(8.0);
crate::rtl::grid(ui, egui::Grid::new("colors_custom").num_columns(2).spacing(vec2(10.0, 5.0)), |ui| {
egui::Grid::new("colors_custom").num_columns(2).spacing(vec2(10.0, 5.0)).show(ui, |ui| {
ui.label(tl!("Color model:"));
craft_ui::ComboBox::from_id_salt("colors_model")
.selected_text(match d.model {
+1 -1
View File
@@ -230,7 +230,7 @@ fn calendar(ui: &mut Ui, t: &Tokens, month: &mut (i64, u32)) -> Option<u32> {
// 1970-01-01 was a Thursday: Monday-first column of the 1st.
let lead = (first + 3).rem_euclid(7) as u32;
let days = wordcraft_engine::cmd::controls::days_in_month(y, m);
crate::rtl::grid(ui, egui::Grid::new("cc_calendar").spacing(vec2(2.0, 2.0)), |ui| {
egui::Grid::new("cc_calendar").spacing(vec2(2.0, 2.0)).show(ui, |ui| {
for cell in 0..(lead + days) {
if cell < lead {
ui.label("");
+30 -34
View File
@@ -249,29 +249,27 @@ pub fn contributors_ui(ui: &mut egui::Ui) {
);
ui.label(RichText::new(summary).small().weak());
ui.separator();
crate::rtl::island(ui, |ui| {
egui::ScrollArea::both().auto_shrink([false, true]).max_height(360.0).show(ui, |ui| {
if list.is_empty() {
ui.label(tl!("No contributor data was built into this copy."));
} else if v.table {
table(ui, &list, &mut v);
} else {
ui.horizontal_wrapped(|ui| {
for (i, c) in list.iter().enumerate() {
if i > 0 {
ui.label(RichText::new("·").weak());
}
ui.hyperlink_to(c.name(v.names), format!("https://github.com/{}", c.login)).on_hover_text(c.summary());
egui::ScrollArea::both().auto_shrink([false, true]).max_height(360.0).show(ui, |ui| {
if list.is_empty() {
ui.label(tl!("No contributor data was built into this copy."));
} else if v.table {
table(ui, &list, &mut v);
} else {
ui.horizontal_wrapped(|ui| {
for (i, c) in list.iter().enumerate() {
if i > 0 {
ui.label(RichText::new("·").weak());
}
});
}
})
ui.hyperlink_to(c.name(v.names), format!("https://github.com/{}", c.login)).on_hover_text(c.summary());
}
});
}
});
ui.data_mut(|d| d.insert_temp(id, v));
}
fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) {
crate::rtl::grid(ui, egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()), |ui| {
egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| {
for k in SortKey::ALL {
let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" };
if ui.button(RichText::new(format!("{}{arrow}", tl!(k.label().1))).strong()).on_hover_text(tl!(k.label().0)).clicked() {
@@ -307,24 +305,22 @@ pub fn models_ui(ui: &mut egui::Ui) {
return;
}
let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1);
crate::rtl::island(ui, |ui| {
egui::ScrollArea::both().auto_shrink([false, true]).max_height(360.0).show(ui, |ui| {
crate::rtl::grid(ui, egui::Grid::new("credits_models").striped(true).num_columns(6), |ui| {
for h in ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"] {
ui.label(RichText::new(tl!(h)).strong());
}
egui::ScrollArea::both().auto_shrink([false, true]).max_height(360.0).show(ui, |ui| {
egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| {
for h in ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"] {
ui.label(RichText::new(tl!(h)).strong());
}
ui.end_row();
for m in MODELS {
ui.label(m.company);
ui.label(m.model);
ui.label(m.version);
ui.label(group(m.commits));
ui.label(format!("{:.0}%", 100.0 * m.commits as f64 / TOTAL_COMMITS.max(assisted) as f64));
ui.label(format!("+{} / −{}", group(m.lines_added), group(m.lines_deleted)));
ui.end_row();
for m in MODELS {
ui.label(m.company);
ui.label(m.model);
ui.label(m.version);
ui.label(group(m.commits));
ui.label(format!("{:.0}%", 100.0 * m.commits as f64 / TOTAL_COMMITS.max(assisted) as f64));
ui.label(format!("+{} / −{}", group(m.lines_added), group(m.lines_deleted)));
ui.end_row();
}
});
})
}
});
});
}
+6 -6
View File
@@ -31,8 +31,8 @@ pub(super) fn bookmark(app: &mut WordApp, ui: &mut Ui, name: &mut String, by_loc
let marks = bookmark_names(&app.session.doc, *by_location, *hidden);
let t = Tokens::get(ui.ctx());
let (mut add, mut delete, mut go_to) = (false, false, false);
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
let name_label = ui.label(tl!("Bookmark name:"));
ui.add(egui::TextEdit::singleline(name).desired_width(240.0)).labelled_by(name_label.id);
egui::Frame::NONE.stroke(egui::Stroke::new(1.0, t.border)).inner_margin(4).show(ui, |ui| {
@@ -51,7 +51,7 @@ pub(super) fn bookmark(app: &mut WordApp, ui: &mut Ui, name: &mut String, by_loc
});
});
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.add_space(18.0);
let listed = marks.iter().any(|n| n == name.trim());
let w = vec2(90.0, 0.0);
@@ -61,9 +61,9 @@ pub(super) fn bookmark(app: &mut WordApp, ui: &mut Ui, name: &mut String, by_loc
});
});
ui.add_space(6.0);
crate::rtl::horizontal_top(ui, |ui| {
ui.horizontal_top(|ui| {
ui.label(tl!("Sort by:"));
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.radio_value(by_location, false, tl!("Name"));
ui.radio_value(by_location, true, tl!("Location"));
});
@@ -84,7 +84,7 @@ pub(super) fn bookmark(app: &mut WordApp, ui: &mut Ui, name: &mut String, by_loc
ui.add_space(8.0);
let mut close = ui.input(|i| i.key_pressed(egui::Key::Escape));
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
close |= ui.button(if *deleted { tl!("Close") } else { tl!("Cancel") }).clicked();
});
});
+3 -3
View File
@@ -137,7 +137,7 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) {
let window = crate::window_close::window(ctx, egui::Window::new(tl!(title)).id(id).collapsible(false).resizable(false));
let shown = place_dialog(ctx, id, window).show(ctx, |ui| {
ui.set_min_width(340.0);
close = crate::rtl::root(ui, |ui| body(app, ui, &mut d));
close = body(app, ui, &mut d);
});
if let Some(shown) = &shown {
if let Dialog::SaveChanges { name, message, .. } = &d {
@@ -187,7 +187,7 @@ pub(crate) fn buttons(ui: &mut Ui, ok: &str) -> (bool, bool) {
let mut r = (false, false);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// Enter and Escape belong to the Colors dialog while it is open above this one.
let keys = !crate::colors::is_open(ui.ctx());
let (okb, cancelb) = ok_cancel_responses(ui, |ui| ui.add(crate::widgets::primary_button(ui, ok)), tl!("Cancel"));
@@ -206,7 +206,7 @@ pub(crate) fn buttons(ui: &mut Ui, ok: &str) -> (bool, bool) {
pub(super) fn close_button(ui: &mut Ui) -> bool {
ui.add_space(8.0);
let mut close = ui.input(|i| i.key_pressed(egui::Key::Escape));
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
close |= ui.add(crate::widgets::primary_button(ui, tl!("Close"))).clicked();
});
close
+2 -2
View File
@@ -198,7 +198,7 @@ pub fn type_ui(app: &mut WordApp, ui: &mut Ui, f: &mut ChartTypeForm) -> bool {
let t = Tokens::get(ui.ctx());
let col = accents(app);
let mut chosen = None;
crate::rtl::grid(ui, egui::Grid::new("chart_types").spacing(vec2(8.0, 8.0)), |ui| {
egui::Grid::new("chart_types").spacing(vec2(8.0, 8.0)).show(ui, |ui| {
for (k, kind) in ChartType::ALL.into_iter().enumerate() {
let (rect, resp) = ui.allocate_exact_size(vec2(112.0, 92.0), Sense::click());
let on = f.kind == kind;
@@ -256,7 +256,7 @@ pub fn data_ui(app: &mut WordApp, ui: &mut Ui, f: &mut ChartDataForm) -> bool {
let mut remove_row = None;
let (can_remove_series, can_remove_row) = (f.names.len() > 1, f.categories.len() > 1);
egui::ScrollArea::horizontal().id_salt("chart_data_h").max_width(760.0).show(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.add_sized([CELL_W, ROW_H - 4.0], egui::Label::new(egui::RichText::new(tl!("Categories")).strong()));
ui.allocate_exact_size(vec2(X_W, ROW_H - 4.0), Sense::hover());
@@ -113,7 +113,7 @@ impl ControlForm {
/// Draws the dialog; returns true to close it.
pub fn body(app: &mut WordApp, ui: &mut Ui, f: &mut ControlForm) -> bool {
ui.set_width(420.0);
crate::rtl::grid(ui, egui::Grid::new("cc_props").num_columns(2).spacing([10.0, 6.0]), |ui| {
egui::Grid::new("cc_props").num_columns(2).spacing([10.0, 6.0]).show(ui, |ui| {
ui.label(tl!("Title"));
ui.add(egui::TextEdit::singleline(&mut f.title).desired_width(280.0));
ui.end_row();
@@ -163,7 +163,7 @@ pub fn body(app: &mut WordApp, ui: &mut Ui, f: &mut ControlForm) -> bool {
let mut done = false;
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// #1906: no forced label color; the theme button text reads on the theme fill.
let (okb, cancelb) = crate::dialogs::ok_cancel_responses(ui, |ui| ui.button(tl!("OK")), tl!("Cancel"));
if cancelb.clicked() || crate::dialogs::cancel_key_pressed(ui) {
@@ -184,7 +184,7 @@ fn entries(ui: &mut Ui, f: &mut ControlForm) {
ui.add_space(6.0);
ui.label(egui::RichText::new(tl!("List entries")).font(semibold(12.0)));
let mut remove = None;
crate::rtl::grid(ui, egui::Grid::new("cc_entries").num_columns(3).spacing([6.0, 4.0]), |ui| {
egui::Grid::new("cc_entries").num_columns(3).spacing([6.0, 4.0]).show(ui, |ui| {
ui.label(egui::RichText::new(tl!("Display text")).small());
ui.label(egui::RichText::new(tl!("Value")).small());
ui.end_row();
+1 -1
View File
@@ -99,7 +99,7 @@ pub(crate) fn buttons(ui: &mut Ui, ok: &str, cancel: &str, ok_enabled: bool) ->
let mut r = (false, false);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (okb, cancelb) =
crate::dialogs::ok_cancel_responses(ui, |ui| ui.add_enabled(ok_enabled, crate::widgets::primary_button(ui, tl!(ok))), tl!(cancel));
if cancelb.clicked() || crate::dialogs::cancel_key_pressed(ui) {
@@ -62,7 +62,7 @@ pub(super) fn drop_cap_ui(app: &mut WordApp, ui: &mut Ui, f: &mut DropCapForm) -
let preview = app.previews.font_preview_fn();
let unit = crate::options::unit();
ui.add_enabled_ui(f.position != "none", |ui| {
crate::rtl::grid(ui, egui::Grid::new("dropcap").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("dropcap").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Font:"));
if let Some(v) = crate::widgets::font_combo(ui, "dropcap_font", 190.0, &f.font, &fams, Some(&*preview))
&& v != crate::widgets::ADD_FONT
@@ -380,11 +380,11 @@ pub(super) fn columns_ui(app: &mut WordApp, ui: &mut Ui, f: &mut ColumnsForm) ->
});
ui.add_space(6.0);
ui.label(egui::RichText::new(tl!("Width and spacing")).font(semibold(12.5)));
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
let mut changed = None;
egui::ScrollArea::vertical().id_salt("columns_widths").max_height(130.0).show(ui, |ui| {
crate::rtl::grid(ui, egui::Grid::new("columns_grid").num_columns(3).spacing(vec2(12.0, 4.0)), |ui| {
egui::Grid::new("columns_grid").num_columns(3).spacing(vec2(12.0, 4.0)).show(ui, |ui| {
ui.label(tl!("Col #:"));
ui.label(tl!("Width:"));
ui.label(tl!("Spacing:"));
@@ -416,7 +416,7 @@ pub(super) fn columns_ui(app: &mut WordApp, ui: &mut Ui, f: &mut ColumnsForm) ->
f.rebalance(i);
}
let mut equal = f.equal;
if ui.add_enabled_ui(f.count > 1, |ui| ui.checkbox(&mut equal, tl!("Equal column width"))).inner.changed() {
if ui.add_enabled(f.count > 1, egui::Checkbox::new(&mut equal, tl!("Equal column width"))).changed() {
f.equal = equal;
if equal {
f.even_out();
@@ -424,7 +424,7 @@ pub(super) fn columns_ui(app: &mut WordApp, ui: &mut Ui, f: &mut ColumnsForm) ->
}
});
ui.add_space(8.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(tl!("Preview"));
columns_preview(ui, f, &t);
});
@@ -264,9 +264,9 @@ fn merge_field_name(code: &str) -> String {
pub(super) fn field_ui(app: &mut WordApp, ui: &mut Ui, f: &mut FieldForm) -> bool {
let t = Tokens::get(ui.ctx());
ui.set_width(620.0);
crate::rtl::horizontal_top(ui, |ui| {
ui.horizontal_top(|ui| {
// Left: category and field names.
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.set_width(200.0);
ui.label(egui::RichText::new(tl!("Pick a field from the list")).font(semibold(12.5)));
ui.label(tl!("Categories:"));
@@ -293,7 +293,7 @@ pub(super) fn field_ui(app: &mut WordApp, ui: &mut Ui, f: &mut FieldForm) -> boo
});
ui.add_space(14.0);
// Right: the field's options, or its code.
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Field properties")).font(semibold(12.5)));
if let Some((_, _, desc)) = FIELDS.iter().find(|(n, _, _)| *n == f.name) {
ui.label(egui::RichText::new(format!("{} {}", tl!("Description:"), tl!(desc))).small().color(t.text_dim));
@@ -388,7 +388,7 @@ pub(super) fn symbol_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SymbolForm) -> b
ui.horizontal(|ui| {
let name = f.selected.map(|c| format!("U+{:04X}", c as u32)).unwrap_or_default();
ui.label(egui::RichText::new(name).font(semibold(12.0)));
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(tl!("from Unicode (hex)"));
let resp = ui.add(egui::TextEdit::singleline(&mut f.code).desired_width(70.0).font(egui::TextStyle::Monospace));
if resp.changed() {
@@ -427,7 +427,7 @@ pub(super) fn symbol_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SymbolForm) -> b
fn special_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SymbolForm, insert: &mut Option<(Value, String, String)>, t: &Tokens) -> bool {
let mut double = false;
egui::ScrollArea::vertical().id_salt("special_chars").max_height(CELL * GRID_ROWS as f32 + 60.0).auto_shrink([false, false]).show(ui, |ui| {
crate::rtl::grid(ui, egui::Grid::new("special_grid").num_columns(3).striped(true).spacing(vec2(14.0, 4.0)), |ui| {
egui::Grid::new("special_grid").num_columns(3).striped(true).spacing(vec2(14.0, 4.0)).show(ui, |ui| {
ui.label(egui::RichText::new(tl!("Character")).font(semibold(12.0)));
ui.label("");
ui.label(egui::RichText::new(tl!("Shortcut key:")).font(semibold(12.0)));
@@ -200,8 +200,8 @@ pub fn customize_keyboard(app: &mut WordApp, ui: &mut Ui, f: &mut KeyboardForm)
if cmds.is_empty() {
ui.label(egui::RichText::new(tl!("No matching commands")).weak());
}
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(tl!("Categories:"));
if let Some(c) = list(ui, "kb_categories", vec2(left, 170.0), &cats, &f.category) {
f.category = c;
@@ -210,7 +210,7 @@ pub fn customize_keyboard(app: &mut WordApp, ui: &mut Ui, f: &mut KeyboardForm)
f.current.clear();
}
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(tl!("Commands:"));
if let Some(c) = list(ui, "kb_commands", vec2(right, 170.0), &cmds, &f.command) {
f.command = c;
@@ -225,15 +225,15 @@ pub fn customize_keyboard(app: &mut WordApp, ui: &mut Ui, f: &mut KeyboardForm)
f.current = keys.first().cloned().unwrap_or_default();
}
let mut close = false;
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(tl!("Keys for this command:"));
let rows: Vec<(String, String)> = keys.iter().map(|k| (k.clone(), key_text(k))).collect();
if let Some(k) = list(ui, "kb_current", vec2(left, 70.0), &rows, &f.current) {
f.current = k;
}
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(tl!("New shortcut:"));
close = capture(ui, f);
ui.add_space(4.0);
@@ -280,7 +280,7 @@ pub fn customize_keyboard(app: &mut WordApp, ui: &mut Ui, f: &mut KeyboardForm)
if ui.add_enabled(!app.session.keymap.is_empty(), egui::Button::new(tl!("Reset All…"))).clicked() {
f.confirm_reset = true;
}
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Close")).clicked() {
close = true;
}
+1 -1
View File
@@ -143,7 +143,7 @@ fn buttons(ui: &mut Ui, ok: &str, ok_enabled: bool) -> (bool, bool) {
let mut r = (false, false);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (okb, cancelb) =
crate::dialogs::ok_cancel_responses(ui, |ui| ui.add_enabled(ok_enabled, crate::widgets::primary_button(ui, tl!(ok))), tl!("Cancel"));
if cancelb.clicked() || crate::dialogs::cancel_key_pressed(ui) {
@@ -164,7 +164,7 @@ pub(super) fn size_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SizeForm) -> bool
(changed && v != was).then_some(v)
};
heading(ui, "Size");
crate::rtl::grid(ui, egui::Grid::new("size_lengths").num_columns(4).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("size_lengths").num_columns(4).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
if let Some(h) = length(ui, "Height:", f.height) {
f.set_height(h);
}
@@ -175,7 +175,7 @@ pub(super) fn size_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SizeForm) -> bool
});
heading(ui, "Scale");
let (sw, sh) = f.scale();
crate::rtl::grid(ui, egui::Grid::new("size_scale").num_columns(4).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("size_scale").num_columns(4).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
if let Some(p) = percent(ui, "Height:", sh) {
f.set_scale_height(p);
}
@@ -185,7 +185,7 @@ pub(super) fn size_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SizeForm) -> bool
ui.end_row();
});
ui.checkbox(&mut f.lock, tl!("Lock aspect ratio"));
ui.add_enabled_ui(f.original.is_some(), |ui| ui.checkbox(&mut f.relative, tl!("Relative to original picture size")));
ui.add_enabled(f.original.is_some(), egui::Checkbox::new(&mut f.relative, tl!("Relative to original picture size")));
if let Some((w, h)) = f.uncropped {
heading(ui, "Original size");
ui.horizontal(|ui| {
@@ -49,7 +49,7 @@ pub(super) fn envelopes_ui(app: &mut WordApp, ui: &mut Ui, f: &mut EnvelopeForm)
ui.add_space(8.0);
let (mut add, mut new, mut cancel) = (false, false, false);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
cancel = ui.button(tl!("Cancel")).clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape));
new = ui.add_enabled(ready, egui::Button::new(tl!("New Document"))).clicked();
add = ui.add_enabled(ready, crate::widgets::primary_button(ui, tl!("Add to Document"))).clicked();
@@ -126,7 +126,7 @@ pub(super) fn sort_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SortForm) -> bool
ui.selectable_value(&mut f.column, i, label);
}
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.radio_value(&mut f.descending, false, tl!("Ascending"));
ui.radio_value(&mut f.descending, true, tl!("Descending"));
});
@@ -253,7 +253,7 @@ fn alert(ui: &mut Ui, text: &Option<String>) {
pub(super) fn split_cells_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SplitCellsForm) -> bool {
// Several cells split one by one keep their rows.
let rows_on = f.cells <= 1 || f.merge_first;
crate::rtl::grid(ui, egui::Grid::new("split_cells").num_columns(2), |ui| {
egui::Grid::new("split_cells").num_columns(2).show(ui, |ui| {
ui.label(tl!("Number of columns:"));
ui.add(egui::DragValue::new(&mut f.columns).range(1..=63));
ui.end_row();
@@ -262,7 +262,7 @@ pub(super) fn split_cells_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SplitCellsF
ui.end_row();
});
let mut merge = f.merge_first;
if ui.add_enabled_ui(f.several, |ui| ui.checkbox(&mut merge, tl!("Merge cells before split"))).inner.changed() {
if ui.add_enabled(f.several, egui::Checkbox::new(&mut merge, tl!("Merge cells before split"))).changed() {
f.set_merge(merge);
}
alert(ui, &f.error);
@@ -379,7 +379,7 @@ impl TextToTableForm {
pub(super) fn text_to_table_ui(app: &mut WordApp, ui: &mut Ui, f: &mut TextToTableForm) -> bool {
let before = (f.separator.clone(), f.other_char.clone(), f.columns);
heading(ui, "Table size");
crate::rtl::grid(ui, egui::Grid::new("text_to_table").num_columns(2), |ui| {
egui::Grid::new("text_to_table").num_columns(2).show(ui, |ui| {
ui.label(tl!("Number of columns:"));
ui.add(egui::DragValue::new(&mut f.columns).range(1..=63));
ui.end_row();
@@ -395,7 +395,7 @@ pub(super) fn text_to_table_ui(app: &mut WordApp, ui: &mut Ui, f: &mut TextToTab
ui.radio_value(&mut f.autofit, "contents".to_string(), tl!("AutoFit to contents"));
ui.radio_value(&mut f.autofit, "window".to_string(), tl!("AutoFit to window"));
heading(ui, "Separate text at");
crate::rtl::grid(ui, egui::Grid::new("text_to_table_sep").num_columns(2), |ui| {
egui::Grid::new("text_to_table_sep").num_columns(2).show(ui, |ui| {
ui.radio_value(&mut f.separator, "paragraph".to_string(), tl!("Paragraphs"));
ui.radio_value(&mut f.separator, "comma".to_string(), tl!("Commas"));
ui.end_row();
@@ -50,7 +50,7 @@ impl LineNumbersForm {
pub(super) fn line_numbers_ui(app: &mut WordApp, ui: &mut Ui, f: &mut LineNumbersForm) -> bool {
ui.checkbox(&mut f.on, tl!("Add line numbering"));
ui.add_enabled_ui(f.on, |ui| {
crate::rtl::grid(ui, egui::Grid::new("lnum").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("lnum").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Start at:"));
ui.add(egui::DragValue::new(&mut f.start).range(1..=32_767));
ui.end_row();
@@ -112,7 +112,7 @@ impl HyphenationForm {
pub(super) fn hyphenation_ui(app: &mut WordApp, ui: &mut Ui, f: &mut HyphenationForm) -> bool {
ui.checkbox(&mut f.auto, tl!("Automatically hyphenate document"));
ui.checkbox(&mut f.caps, tl!("Hyphenate words in CAPS"));
crate::rtl::grid(ui, egui::Grid::new("hyph").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("hyph").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Hyphenation zone:"));
ui.add(egui::DragValue::new(&mut f.zone).speed(0.01).range(0.0..=22.0 * 72.0 / unit().pt_per_unit()).measure(unit()).max_decimals(2));
ui.end_row();
@@ -236,7 +236,7 @@ pub(super) fn manual_ui(app: &mut WordApp, ui: &mut Ui, f: &mut ManualForm) -> b
ui.add_space(8.0);
let mut close = false;
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Cancel")).clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
close = true;
}
+14 -16
View File
@@ -171,8 +171,8 @@ pub fn define_list(app: &mut WordApp, ui: &mut Ui, f: &mut ListForm) -> bool {
ui.add_space(4.0);
ui.label(egui::RichText::new(tl!(s)).font(semibold(12.5)));
};
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(tl!("Click level to modify:"));
for i in 0..LEVELS {
if ui.selectable_label(f.level == i, format!("{}", i + 1)).clicked() {
@@ -180,7 +180,7 @@ pub fn define_list(app: &mut WordApp, ui: &mut Ui, f: &mut ListForm) -> bool {
}
}
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Preview")).small().weak());
preview(ui, f);
});
@@ -189,7 +189,7 @@ pub fn define_list(app: &mut WordApp, ui: &mut Ui, f: &mut ListForm) -> bool {
let styles = f.styles.clone();
let Some(l) = f.levels.get_mut(i) else { return true };
heading(ui, "Number format");
crate::rtl::grid(ui, egui::Grid::new("mll_number").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("mll_number").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Enter formatting for number:"));
ui.text_edit_singleline(&mut l.text);
ui.end_row();
@@ -257,7 +257,7 @@ pub fn define_list(app: &mut WordApp, ui: &mut Ui, f: &mut ListForm) -> bool {
ui.end_row();
});
heading(ui, "Position");
crate::rtl::grid(ui, egui::Grid::new("mll_position").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("mll_position").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Number alignment:"));
ui.horizontal(|ui| {
for (v, s) in [("left", "Left"), ("center", "Center"), ("right", "Right")] {
@@ -283,7 +283,7 @@ pub fn define_list(app: &mut WordApp, ui: &mut Ui, f: &mut ListForm) -> bool {
}
});
ui.end_row();
ui.add_enabled_ui(l.follow == "tab", |ui| ui.checkbox(&mut l.tab_on, tl!("Add tab stop at:")));
ui.add_enabled(l.follow == "tab", egui::Checkbox::new(&mut l.tab_on, tl!("Add tab stop at:")));
ui.add_enabled(l.follow == "tab" && l.tab_on, len(&mut l.tab_at, unit));
ui.end_row();
});
@@ -455,13 +455,11 @@ fn color(ui: &mut Ui, theme: &[wordcraft_doc::Rgb], value: &mut String, none_lab
ui.painter().rect_stroke(r, 2.0, egui::Stroke::new(1.0, t.border_strong), egui::StrokeKind::Inside);
let label = if c.is_some() { value.clone() } else { tl!(none_label).to_string() };
ui.menu_button(label, |ui| {
crate::rtl::root(ui, |ui| {
if ui.button(tl!(none_label)).clicked() {
value.clear();
ui.close();
}
crate::colors::field_menu(ui, theme, id, value);
})
if ui.button(tl!(none_label)).clicked() {
value.clear();
ui.close();
}
crate::colors::field_menu(ui, theme, id, value);
});
});
}
@@ -512,7 +510,7 @@ pub fn track_options(app: &mut WordApp, ui: &mut Ui, f: &mut TrackForm) -> bool
ui.label(egui::RichText::new(tl!(s)).font(semibold(12.5)));
};
heading(ui, "Show");
crate::rtl::grid(ui, egui::Grid::new("tco_show").num_columns(2).spacing(vec2(16.0, 4.0)), |ui| {
egui::Grid::new("tco_show").num_columns(2).spacing(vec2(16.0, 4.0)).show(ui, |ui| {
ui.checkbox(&mut f.comments, tl!("Comments"));
ui.checkbox(&mut f.ink, tl!("Ink"));
ui.end_row();
@@ -533,7 +531,7 @@ pub fn track_options(app: &mut WordApp, ui: &mut Ui, f: &mut TrackForm) -> bool
ui.radio_value(&mut f.comment_display, v.to_string(), tl!(l));
}
heading(ui, "Advanced");
crate::rtl::grid(ui, egui::Grid::new("tco_advanced").num_columns(3).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("tco_advanced").num_columns(3).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Insertions:"));
choice(ui, "tco_ins", &mut f.insert_mark, INSERT_MARKS);
color(ui, &theme, &mut f.insert_color, "By author", None);
@@ -553,7 +551,7 @@ pub fn track_options(app: &mut WordApp, ui: &mut Ui, f: &mut TrackForm) -> bool
ui.end_row();
});
heading(ui, "Moves");
crate::rtl::grid(ui, egui::Grid::new("tco_moves").num_columns(3).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("tco_moves").num_columns(3).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Moved from:"));
choice(ui, "tco_move_from", &mut f.move_from_mark, DELETE_MARKS);
ui.push_id("tco_move_from_color", |ui| color(ui, &theme, &mut f.move_from_color, "By author", None));
+8 -11
View File
@@ -86,7 +86,7 @@ pub fn note_options(app: &mut WordApp, ui: &mut Ui, f: &mut NoteForm) -> bool {
};
heading(ui, "Location");
let mut convert = None;
crate::rtl::grid(ui, egui::Grid::new("notes_location").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("notes_location").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.radio_value(&mut f.endnote, false, tl!("Footnotes"));
ui.add_enabled_ui(!f.endnote, |ui| {
choice(ui, "notes_fpos", &mut f.pos[0], &[("pageBottom", "Bottom of Page"), ("beneathText", "Below text")], true);
@@ -99,15 +99,12 @@ pub fn note_options(app: &mut WordApp, ui: &mut Ui, f: &mut NoteForm) -> bool {
ui.end_row();
ui.label("");
ui.menu_button(format!("{}…", tl!("Convert")), |ui| {
crate::rtl::root(ui, |ui| {
for (to, l) in [("endnote", "Footnotes to endnotes"), ("footnote", "Endnotes to footnotes"), ("swap", "Swap footnotes and endnotes")]
{
if ui.button(tl!(l)).clicked() {
convert = Some(to);
ui.close();
}
for (to, l) in [("endnote", "Footnotes to endnotes"), ("footnote", "Endnotes to footnotes"), ("swap", "Swap footnotes and endnotes")] {
if ui.button(tl!(l)).clicked() {
convert = Some(to);
ui.close();
}
})
}
});
ui.end_row();
});
@@ -118,7 +115,7 @@ pub fn note_options(app: &mut WordApp, ui: &mut Ui, f: &mut NoteForm) -> bool {
}
heading(ui, "Format");
let k = usize::from(f.endnote);
crate::rtl::grid(ui, egui::Grid::new("notes_format").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("notes_format").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Number format"));
if let Some(v) = f.fmt.get_mut(k) {
choice(ui, "notes_fmt", v, &FORMATS, false);
@@ -150,7 +147,7 @@ pub fn note_options(app: &mut WordApp, ui: &mut Ui, f: &mut NoteForm) -> bool {
let (mut insert, mut apply, mut cancel) = (false, false, false);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let label = if f.endnote { "Insert Endnote" } else { "Insert Footnote" };
let enter = ui.input(|i| i.key_pressed(egui::Key::Enter));
if cfg!(target_os = "macos") {
+7 -7
View File
@@ -196,7 +196,7 @@ pub fn tabs(app: &mut WordApp, ui: &mut Ui, f: &mut TabsForm) -> bool {
let u = unit();
let k = u.pt_per_unit();
ui.horizontal(|ui| {
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
if ui.button(tl!("New")).clicked() {
f.new_stop();
@@ -283,7 +283,7 @@ pub fn tabs(app: &mut WordApp, ui: &mut Ui, f: &mut TabsForm) -> bool {
f.set_leader(i, l);
pick = Some(i);
}
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(10.0); // clear of the scrollbar
if ui.add(egui::Button::new("×").small().frame(false)).on_hover_text(tl!("Clear")).clicked() {
remove = Some(i);
@@ -320,7 +320,7 @@ pub fn tabs(app: &mut WordApp, ui: &mut Ui, f: &mut TabsForm) -> bool {
}
});
ui.add_space(12.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.set_min_width(130.0);
let default_label = ui.label(tl!("Default tab stops:"));
ui.add(egui::DragValue::new(&mut f.default_tab).speed(0.05).range(1.0 / k..=1584.0 / k).measure(u).max_decimals(2))
@@ -617,8 +617,8 @@ impl From<bool> for Target {
/// Setting, style, colour and width on the left; the preview with its side toggles on the right.
fn lines(ui: &mut Ui, id: &str, l: &mut LineForm, theme: &[Rgb], fill: Option<Rgb>, target: Target) {
let between = target == Target::Paragraph;
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Setting:")).font(semibold(12.5)));
for (s, label) in
[(Setting::None, "None"), (Setting::Box, "Box"), (Setting::Shadow, "Shadow"), (Setting::ThreeD, "3-D"), (Setting::Custom, "Custom")]
@@ -629,7 +629,7 @@ fn lines(ui: &mut Ui, id: &str, l: &mut LineForm, theme: &[Rgb], fill: Option<Rg
}
});
ui.add_space(8.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(tl!("Style:"));
let current = STYLES.iter().find(|(s, _)| *s == l.style).map(|(_, n)| *n).unwrap_or("Single");
craft_ui::ComboBox::from_id_salt((id, "style")).selected_text(tl!(current)).width(120.0).show_ui(ui, |ui| {
@@ -650,7 +650,7 @@ fn lines(ui: &mut Ui, id: &str, l: &mut LineForm, theme: &[Rgb], fill: Option<Rg
});
});
ui.add_space(8.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Preview")).font(semibold(12.5)));
if let Some(side) = preview(ui, l, fill, target) {
l.toggle(side);
@@ -105,7 +105,7 @@ pub(super) fn caption_ui(app: &mut WordApp, ui: &mut Ui, f: &mut CaptionForm) ->
});
ui.add_space(6.0);
let mut then = None;
crate::rtl::grid(ui, egui::Grid::new("cap_opts").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("cap_opts").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let label_label = ui.label(tl!("Label:"));
let labels: Vec<(String, &str)> = f.labels.iter().map(|l| (l.clone(), l.as_str())).collect();
let combo = craft_ui::ComboBox::from_id_salt("cap_label").selected_text(tl!(&f.label)).width(150.0).show_ui(ui, |ui| {
@@ -150,7 +150,7 @@ pub(super) fn caption_ui(app: &mut WordApp, ui: &mut Ui, f: &mut CaptionForm) ->
/// Draws the caption numbering format, chapter-number toggle and chapter level and separator
/// controls, returning to the caption dialog with the edited or original form.
pub(super) fn numbering_ui(ui: &mut Ui, f: &mut CaptionForm, was: &CaptionForm) -> Next {
crate::rtl::grid(ui, egui::Grid::new("cap_num").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("cap_num").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let format_label = ui.label(tl!("Format:"));
let opts: Vec<(String, &str)> = NUMBER_FORMATS.iter().map(|(v, l)| (v.to_string(), *l)).collect();
choice(ui, "cap_fmt", &mut f.format, &opts, &format_label);
@@ -158,7 +158,7 @@ pub(super) fn numbering_ui(ui: &mut Ui, f: &mut CaptionForm, was: &CaptionForm)
});
ui.checkbox(&mut f.chapter, tl!("Add the chapter number"));
ui.add_enabled_ui(f.chapter, |ui| {
crate::rtl::grid(ui, egui::Grid::new("cap_chap").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("cap_chap").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let chapter_label = ui.label(tl!("Chapters begin at:"));
let opts: Vec<(u8, String)> = (1..=9u8).map(|l| (l, crate::i18n::fmt(tl!("Heading {n}"), &[("n", &l.to_string())]))).collect();
let shown = opts.iter().find(|(l, _)| *l == f.chapter_level).map(|(_, s)| s.clone()).unwrap_or_default();
@@ -65,7 +65,7 @@ impl CrossRefForm {
/// Insert.
pub(super) fn cross_ref_ui(app: &mut WordApp, ui: &mut Ui, f: &mut CrossRefForm) -> Next {
let was = f.to.clone();
crate::rtl::grid(ui, egui::Grid::new("xref_opts").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("xref_opts").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let type_label = ui.label(tl!("Reference type:"));
let types = ref_types(app);
let types: Vec<(String, &str)> = types.iter().map(|(v, l)| (v.clone(), l.as_str())).collect();
@@ -68,7 +68,7 @@ pub(super) fn index_ui(app: &mut WordApp, ui: &mut Ui, f: &mut IndexForm) -> Nex
};
sample(ui, &lines, f.right_align, leader_char(&f.leader));
ui.add_space(6.0);
crate::rtl::grid(ui, egui::Grid::new("idx").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("idx").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Type:"));
ui.horizontal(|ui| {
ui.radio_value(&mut f.kind, "indented".to_string(), tl!("Indented"));
@@ -153,7 +153,7 @@ impl MarkForm {
/// and Mark All running `references.markEntry`.
pub(super) fn mark_ui(app: &mut WordApp, ui: &mut Ui, f: &mut MarkForm) -> Next {
heading(ui, "Index");
crate::rtl::grid(ui, egui::Grid::new("xe").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("xe").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let entry_label = ui.label(tl!("Main entry:"));
ui.add(egui::TextEdit::singleline(&mut f.entry).desired_width(220.0)).labelled_by(entry_label.id);
ui.end_row();
@@ -191,7 +191,7 @@ pub(super) fn mark_ui(app: &mut WordApp, ui: &mut Ui, f: &mut MarkForm) -> Next
let mut close = false;
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Close")).clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
close = true;
}
@@ -170,7 +170,7 @@ impl SourceForm {
/// Draws a source's type and fields, optionally every bibliography field, and adds it as a
/// bibliography source or inserts a citation on OK.
pub(super) fn source_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SourceForm) -> Next {
crate::rtl::grid(ui, egui::Grid::new("src").num_columns(2).spacing(vec2(10.0, 5.0)), |ui| {
egui::Grid::new("src").num_columns(2).spacing(vec2(10.0, 5.0)).show(ui, |ui| {
let kind_label = ui.label(tl!("Source type:"));
let shown = SOURCE_TYPES.iter().find(|(k, ..)| *k == f.source.kind).map(|(_, l, _)| tl!(l).to_string()).unwrap_or_default();
let combo = craft_ui::ComboBox::from_id_salt("src_kind").selected_text(shown).width(200.0).show_ui(ui, |ui| {
@@ -286,7 +286,7 @@ pub(super) fn sources_ui(app: &mut WordApp, ui: &mut Ui, selected: &mut Option<u
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Close")).clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
next = Next::Close;
}
@@ -145,7 +145,7 @@ pub(super) fn toc_options_ui(app: &mut WordApp, ui: &mut Ui, f: &mut TocForm, wa
// A solid scroll bar takes its own room beside the list instead of floating over the level boxes.
ui.spacing_mut().scroll = egui::style::ScrollStyle::solid();
egui::ScrollArea::vertical().max_height(180.0).show(ui, |ui| {
crate::rtl::grid(ui, egui::Grid::new("toc_styles").num_columns(2).spacing(vec2(10.0, 3.0)), |ui| {
egui::Grid::new("toc_styles").num_columns(2).spacing(vec2(10.0, 3.0)).show(ui, |ui| {
for (name, level) in f.style_levels.iter_mut() {
// Built-in styles read in the interface language; the document keeps the English name.
let shown = app.session.doc.styles.styles.iter().find(|st| st.name == *name).map_or_else(|| name.clone(), crate::i18n::style_name);
+12 -12
View File
@@ -67,7 +67,7 @@ fn buttons(ui: &mut Ui, ok_enabled: bool) -> (bool, bool) {
let mut r = (false, false);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (okb, cancelb) = crate::dialogs::ok_cancel_responses(
ui,
|ui| ui.add_enabled(ok_enabled, crate::widgets::primary_button(ui, tl!("OK"))),
@@ -197,8 +197,8 @@ fn compare_ui(app: &mut WordApp, ui: &mut Ui, f: &mut CompareForm) -> bool {
if !browse {
ui.label(egui::RichText::new(tl!("Comparing with another file needs the desktop app.")).small().weak());
}
crate::rtl::grid(ui, egui::Grid::new("compare_files").num_columns(2).spacing(vec2(16.0, 4.0)), |ui| {
crate::rtl::vertical(ui, |ui| {
egui::Grid::new("compare_files").num_columns(2).spacing(vec2(16.0, 4.0)).show(ui, |ui| {
ui.vertical(|ui| {
heading(ui, "Original document");
ui.radio_value(&mut f.original_current, true, tl!("This document"));
ui.horizontal(|ui| {
@@ -209,7 +209,7 @@ fn compare_ui(app: &mut WordApp, ui: &mut Ui, f: &mut CompareForm) -> bool {
}
});
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
heading(ui, "Revised document");
ui.horizontal(|ui| {
ui.add_enabled(browse, egui::TextEdit::singleline(&mut f.revised).desired_width(170.0));
@@ -230,7 +230,7 @@ fn compare_ui(app: &mut WordApp, ui: &mut Ui, f: &mut CompareForm) -> bool {
}
if f.more {
heading(ui, "Comparison settings");
crate::rtl::grid(ui, egui::Grid::new("compare_settings").num_columns(2).spacing(vec2(16.0, 2.0)), |ui| {
egui::Grid::new("compare_settings").num_columns(2).spacing(vec2(16.0, 2.0)).show(ui, |ui| {
for (i, (_, label)) in COMPARE_SETTINGS.iter().enumerate() {
if let Some(on) = f.settings.get_mut(i) {
ui.checkbox(on, tl!(label));
@@ -245,13 +245,13 @@ fn compare_ui(app: &mut WordApp, ui: &mut Ui, f: &mut CompareForm) -> bool {
.small()
.weak(),
);
crate::rtl::grid(ui, egui::Grid::new("compare_show").num_columns(2).spacing(vec2(24.0, 2.0)), |ui| {
crate::rtl::vertical(ui, |ui| {
egui::Grid::new("compare_show").num_columns(2).spacing(vec2(24.0, 2.0)).show(ui, |ui| {
ui.vertical(|ui| {
heading(ui, "Show changes at:");
ui.radio_value(&mut f.character_level, false, tl!("Word level"));
ui.radio_value(&mut f.character_level, true, tl!("Character level"));
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
heading(ui, "Show changes in:");
for (v, l) in [("original", "Original document"), ("revised", "Revised document"), ("new", "New document")] {
ui.radio_value(&mut f.show_in, v.to_string(), tl!(l));
@@ -445,7 +445,7 @@ fn autocorrect_tab(app: &mut WordApp, ui: &mut Ui, f: &mut AutoCorrectForm) {
ui.checkbox(&mut o.cap_days, tl!("Capitalize names of days"));
ui.checkbox(&mut o.caps_lock, tl!("Correct accidental usage of cAPS LOCK key"));
ui.checkbox(&mut o.replace_text, tl!("Replace text as you type"));
crate::rtl::grid(ui, egui::Grid::new("ac_boxes").num_columns(2).spacing(vec2(8.0, 2.0)), |ui| {
egui::Grid::new("ac_boxes").num_columns(2).spacing(vec2(8.0, 2.0)).show(ui, |ui| {
let replace_label = ui.label(tl!("Replace:"));
let with_label = ui.label(tl!("With:"));
ui.end_row();
@@ -462,7 +462,7 @@ fn autocorrect_tab(app: &mut WordApp, ui: &mut Ui, f: &mut AutoCorrectForm) {
// #3036: the filter matches nothing — say so instead of showing an empty box.
ui.label(egui::RichText::new(tl!("(none)")).small().color(Tokens::get(ui.ctx()).text_dim));
}
crate::rtl::grid(ui, egui::Grid::new("ac_list_grid").num_columns(2).spacing(vec2(8.0, 1.0)).striped(true), |ui| {
egui::Grid::new("ac_list_grid").num_columns(2).spacing(vec2(8.0, 1.0)).striped(true).show(ui, |ui| {
for (a, b, _) in list.iter().filter(|(a, _, _)| typed.is_empty() || a.to_lowercase().starts_with(&typed)).take(500) {
let selected = f.from == *a;
if ui.add_sized(vec2(110.0, 16.0), egui::Button::selectable(selected, a.as_str())).clicked() {
@@ -543,7 +543,7 @@ fn math_tab(ui: &mut Ui, f: &mut AutoCorrectForm) {
});
let filter = f.math_filter.trim().trim_start_matches('\\').to_string();
egui::ScrollArea::vertical().id_salt("ac_math").max_height(240.0).auto_shrink([false, true]).show(ui, |ui| {
crate::rtl::grid(ui, egui::Grid::new("ac_math_grid").num_columns(2).spacing(vec2(16.0, 1.0)).striped(true), |ui| {
egui::Grid::new("ac_math_grid").num_columns(2).spacing(vec2(16.0, 1.0)).striped(true).show(ui, |ui| {
for (name, c) in wordcraft_doc::math_symbols::SYMBOLS.iter().filter(|(n, _)| filter.is_empty() || n.starts_with(filter.as_str())) {
ui.label(format!("\\{name}"));
ui.label(c.to_string());
@@ -662,7 +662,7 @@ fn setup_ui(app: &mut WordApp, ui: &mut Ui, st: &mut RestrictPane) {
let inside = app.session.doc.perm_ranges().iter().any(|r| r.everyone() && r.start <= a && b <= r.end && r.start.story == a.story);
let mut everyone = inside;
let can = inside || a != b;
if ui.add_enabled_ui(can, |ui| ui.checkbox(&mut everyone, tl!("Everyone"))).inner.changed() {
if ui.add_enabled(can, egui::Checkbox::new(&mut everyone, tl!("Everyone"))).changed() {
let p = if everyone { json!({"exception": "add"}) } else { json!({"exception": "remove"}) };
if let Err(e) = app.run("review.restrict", p) {
st.message = e;
+12 -12
View File
@@ -108,10 +108,10 @@ pub fn customize_ribbon(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm) -> b
if cmds.is_empty() {
ui.label(egui::RichText::new(tl!("No matching commands")).weak());
}
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Choose a command")).font(semibold(12.5)));
crate::rtl::horizontal_top(ui, |ui| {
ui.horizontal_top(|ui| {
if let Some(c) = list(ui, "rb_categories", vec2(left, h), &cats, &f.category) {
f.category = c;
f.filter.clear();
@@ -163,7 +163,7 @@ pub fn customize_ribbon(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm) -> b
if ui.add_enabled(custom, egui::Button::new(tl!("Reset…"))).clicked() {
f.confirm_reset = true;
}
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Close")).clicked() {
close = true;
}
@@ -219,7 +219,7 @@ fn ribbon_page(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm, h: f32) {
let sel = f.selected.clone();
let tab_custom = app.session.ribbon.tabs().iter().any(|t| t.name == sel.tab && t.custom);
// Add >> / << Remove.
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.add_space(h / 2.0 - 10.0);
let can_add = sel.group.is_some() && !f.command.is_empty();
let add = ui.add_enabled(can_add, egui::Button::new(tl!("Add >>")).min_size(vec2(96.0, 0.0)));
@@ -245,11 +245,11 @@ fn ribbon_page(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm, h: f32) {
}
});
ui.add_space(4.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Ribbon tabs")).font(semibold(12.5)));
crate::rtl::horizontal_top(ui, |ui| {
ui.horizontal_top(|ui| {
tree(app, ui, f, &rows, h);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.add_space(h / 2.0 - 10.0);
for (text, by) in [("Up", -1), ("Down", 1)] {
if ui.add(egui::Button::new(tl!(text)).min_size(vec2(56.0, 0.0))).clicked() {
@@ -354,7 +354,7 @@ fn qat_page(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm, h: f32) {
if !ids.contains(&f.qat_selected) {
f.qat_selected = ids.first().cloned().unwrap_or_default();
}
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.add_space(h / 2.0 - 10.0);
let can_add = !f.command.is_empty() && !ids.contains(&f.command);
if ui.add_enabled(can_add, egui::Button::new(tl!("Add >>")).min_size(vec2(96.0, 0.0))).clicked()
@@ -367,14 +367,14 @@ fn qat_page(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm, h: f32) {
}
});
ui.add_space(4.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Quick Access Toolbar")).font(semibold(12.5)));
crate::rtl::horizontal_top(ui, |ui| {
ui.horizontal_top(|ui| {
let rows: Vec<(String, String)> = ids.iter().map(|id| (id.clone(), label_of(app, id))).collect();
if let Some(id) = list(ui, "rb_qat", vec2(250.0, h), &rows, &f.qat_selected) {
f.qat_selected = id;
}
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.add_space(h / 2.0 - 10.0);
for (text, by) in [("Up", -1), ("Down", 1)] {
if ui.add_enabled(!f.qat_selected.is_empty(), egui::Button::new(tl!(text)).min_size(vec2(56.0, 0.0))).clicked() {
@@ -159,7 +159,7 @@ const PICKER_HEIGHT: f32 = 3.0 * 96.0 + 2.0 * 8.0;
fn picker_row(ui: &mut Ui, columns: impl FnOnce(&mut Ui)) {
ui.scope(|ui| {
ui.set_max_height(PICKER_HEIGHT);
crate::rtl::horizontal_top(ui, columns);
ui.horizontal_top(columns);
});
}
@@ -169,7 +169,7 @@ pub fn picker_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SmartArtForm) -> bool {
let theme = app.session.doc.settings.theme_colors.clone();
let mut chosen = None;
picker_row(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.set_width(120.0);
for cat in CATEGORIES {
if ui.selectable_label(f.category == cat, tl!(cat)).clicked() {
@@ -183,10 +183,10 @@ pub fn picker_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SmartArtForm) -> bool {
}
});
ui.separator();
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.set_width(3.0 * 96.0 + 16.0);
ui.set_min_height(2.0 * 96.0 + 8.0);
crate::rtl::grid(ui, egui::Grid::new("smart_art_layouts").spacing(vec2(8.0, 8.0)), |ui| {
egui::Grid::new("smart_art_layouts").spacing(vec2(8.0, 8.0)).show(ui, |ui| {
for (k, layout) in f.shown().into_iter().enumerate() {
let (rect, resp) = ui.allocate_exact_size(vec2(96.0, 96.0), Sense::click());
let on = f.layout == layout;
@@ -241,7 +241,7 @@ pub fn picker_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SmartArtForm) -> bool {
});
});
ui.separator();
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.set_width(200.0);
let (rect, _) = ui.allocate_exact_size(vec2(200.0, 150.0), Sense::hover());
ui.painter().rect(rect, 4.0, t.input, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
@@ -469,7 +469,7 @@ pub fn body(app: &mut WordApp, ui: &mut Ui, f: Form) -> bool {
.collect();
let base_name = styles.iter().find(|(i, _)| i == based_on).map(|(_, n)| n.clone()).unwrap_or_else(|| based_on.clone());
let mut rebased = false;
crate::rtl::grid(ui, egui::Grid::new("tstyle").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("tstyle").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let name_label = ui.label(tl!("Name:"));
ui.text_edit_singleline(name).labelled_by(name_label.id);
ui.end_row();
@@ -567,7 +567,7 @@ pub fn body(app: &mut WordApp, ui: &mut Ui, f: Form) -> bool {
});
ui.add_space(12.0);
ui.label(tl!("Alignment:"));
crate::rtl::grid(ui, egui::Grid::new("tstyle_align").spacing(vec2(1.0, 1.0)), |ui| {
egui::Grid::new("tstyle_align").spacing(vec2(1.0, 1.0)).show(ui, |ui| {
for (i, ((n, v, h), tip)) in CELL_ALIGNS.iter().zip(ALIGN_TIPS).enumerate() {
let on = r.align == *n;
if align_button(ui, (*v, *h), on, tl!(tip)).clicked() {
@@ -181,8 +181,8 @@ pub fn organizer(app: &mut WordApp, ui: &mut Ui, f: &mut OrganizerForm) -> bool
let mut rename = false;
let mut browse = false;
let left_builtin = mine.iter().find(|x| s(x, "id") == f.left).is_none_or(|x| x.get("builtIn").and_then(Value::as_bool).unwrap_or(true));
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(tl!("Document")).font(semibold(12.5)));
list_frame(ui, "organizer_mine", &mut |ui| {
for st in &mine {
@@ -194,16 +194,14 @@ pub fn organizer(app: &mut WordApp, ui: &mut Ui, f: &mut OrganizerForm) -> bool
}
});
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.add_space(60.0);
let w = vec2(96.0, 0.0);
// Point at the Document column (destination of copyFrom): left in LTR, right in RTL.
let copy_arrow = if crate::rtl::flows(ui) { "▶" } else { "◀" };
copy = ui.add_enabled(!f.right.is_empty(), egui::Button::new(format!("{copy_arrow} {}", tl!("Copy"))).min_size(w)).clicked();
copy = ui.add_enabled(!f.right.is_empty(), egui::Button::new(format!("◀ {}", tl!("Copy"))).min_size(w)).clicked();
delete = ui.add_enabled(!left_builtin, egui::Button::new(tl!("Delete")).min_size(w)).clicked();
rename = ui.add_enabled(!left_builtin, egui::Button::new(tl!("Rename…")).min_size(w)).clicked();
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
let name = std::path::Path::new(&f.file).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "—".into());
ui.label(egui::RichText::new(name).font(semibold(12.5))).on_hover_text(f.file.as_str());
list_frame(ui, "organizer_other", &mut |ui| {
@@ -256,7 +254,7 @@ pub fn organizer(app: &mut WordApp, ui: &mut Ui, f: &mut OrganizerForm) -> bool
ui.add_space(8.0);
let mut close = ui.input(|i| i.key_pressed(egui::Key::Escape));
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
close |= ui.add(crate::widgets::primary_button(ui, tl!("Close"))).clicked();
});
});
+129 -167
View File
@@ -137,109 +137,81 @@ fn font_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
if *strike {
fmt.strikethrough = egui::Stroke::new(1.0, preview_color);
}
crate::rtl::Table::new("fontdlg").columns(2).spacing(vec2(10.0, 6.0)).show(ui, |t| {
egui::Grid::new("fontdlg").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
// Word's two font boxes when East Asian text is in use: the Latin text font and
// the Asian text font, each with its own list (type to find a font).
let prev = app.previews.font_preview_fn();
if *asian {
t.row(|r| {
r.cell(|ui| ui.label(tl!("Asian text font:")));
r.cell(|ui| {
let fams = app.previews.east_asian_families();
if let Some(f) = crate::widgets::font_combo(ui, "fontdlg_asian", 220.0, font_ea, &fams, Some(&*prev)) {
if f == crate::widgets::ADD_FONT {
let _ = app.run("font.add", json!({}));
} else if f == crate::widgets::LOAD_SYSTEM_FONTS {
let _ = app.run("font.loadSystem", json!({}));
} else {
*font_ea = f;
}
}
});
});
ui.label(tl!("Asian text font:"));
let fams = app.previews.east_asian_families();
if let Some(f) = crate::widgets::font_combo(ui, "fontdlg_asian", 220.0, font_ea, &fams, Some(&*prev)) {
if f == crate::widgets::ADD_FONT {
let _ = app.run("font.add", json!({}));
} else if f == crate::widgets::LOAD_SYSTEM_FONTS {
let _ = app.run("font.loadSystem", json!({}));
} else {
*font_ea = f;
}
}
ui.end_row();
}
t.row(|r| {
r.cell(|ui| ui.label(if *asian { tl!("Latin text font:") } else { tl!("Font:") }));
r.cell(|ui| {
let fams = app.previews.families();
if let Some(f) = crate::widgets::font_combo(ui, "fontdlg_font", 220.0, font, &fams, Some(&*prev)) {
if f == crate::widgets::ADD_FONT {
let _ = app.run("font.add", json!({}));
} else if f == crate::widgets::LOAD_SYSTEM_FONTS {
let _ = app.run("font.loadSystem", json!({}));
} else {
*font = f;
}
ui.label(if *asian { tl!("Latin text font:") } else { tl!("Font:") });
let fams = app.previews.families();
if let Some(f) = crate::widgets::font_combo(ui, "fontdlg_font", 220.0, font, &fams, Some(&*prev)) {
if f == crate::widgets::ADD_FONT {
let _ = app.run("font.add", json!({}));
} else if f == crate::widgets::LOAD_SYSTEM_FONTS {
let _ = app.run("font.loadSystem", json!({}));
} else {
*font = f;
}
}
ui.end_row();
let size_label = ui.label(tl!("Size:"));
ui.text_edit_singleline(size).labelled_by(size_label.id);
ui.end_row();
ui.label(tl!("Font color:"));
let theme = app.session.doc.settings.theme_colors.clone();
color_field(ui, &theme, color, "Automatic", "auto");
ui.end_row();
let spacing_label = ui.label(tl!("Spacing (pt):"));
ui.add(egui::DragValue::new(spacing).speed(0.1).range(-20.0..=20.0)).labelled_by(spacing_label.id);
ui.end_row();
let label = tl!("Kerning for fonts:");
match kern {
Some(on) => {
ui.checkbox(on, label);
}
None => {
if ui.add(egui::Checkbox::new(&mut false, label).indeterminate(true)).clicked() {
*kern = Some(true);
}
});
});
t.row(|r| {
let size_label = r.cell(|ui| ui.label(tl!("Size:")));
r.cell(|ui| ui.text_edit_singleline(size).labelled_by(size_label.id));
});
t.row(|r| {
r.cell(|ui| ui.label(tl!("Font color:")));
r.cell(|ui| {
let theme = app.session.doc.settings.theme_colors.clone();
color_field(ui, &theme, color, "Automatic", "auto");
});
});
t.row(|r| {
let spacing_label = r.cell(|ui| ui.label(tl!("Spacing (pt):")));
r.cell(|ui| ui.add(egui::DragValue::new(spacing).speed(0.1).range(-20.0..=20.0)).labelled_by(spacing_label.id));
});
t.row(|r| {
let label = tl!("Kerning for fonts:");
r.cell(|ui| match kern {
Some(on) => {
ui.checkbox(on, label);
}
None => {
if ui.add(egui::Checkbox::new(&mut false, label).indeterminate(true)).clicked() {
*kern = Some(true);
}
}
});
r.cell(|ui| {
ui.horizontal(|ui| {
let enabled = *kern == Some(true);
let field = egui::DragValue::new(kern_at).speed(0.5).range(1.0..=1638.0).max_decimals(1);
// The number comes first in the reading order, then "points and above"; a left-to-right
// row in a right-to-left interface runs the other way, so there the label is first.
if crate::rtl::on() && !crate::rtl::flows(ui) {
let points_label = ui.label(tl!("Points and above"));
ui.add_enabled(enabled, field).labelled_by(points_label.id);
} else {
let points = ui.add_enabled(enabled, field);
let points_label = ui.label(tl!("Points and above"));
points.labelled_by(points_label.id);
}
});
});
}
}
ui.horizontal(|ui| {
let points = ui.add_enabled(*kern == Some(true), egui::DragValue::new(kern_at).speed(0.5).range(1.0..=1638.0).max_decimals(1));
let points_label = ui.label(tl!("Points and above"));
points.labelled_by(points_label.id);
});
ui.end_row();
});
ui.separator();
ui.label(egui::RichText::new(tl!("Effects")).font(semibold(12.5)));
crate::rtl::Table::new("fx").columns(2).show(ui, |t| {
t.row(|r| {
r.cell(|ui| ui.checkbox(bold, tl!("Bold")));
r.cell(|ui| ui.checkbox(italic, tl!("Italic")));
});
t.row(|r| {
r.cell(|ui| ui.checkbox(underline, tl!("Underline")));
r.cell(|ui| ui.checkbox(strike, tl!("Strikethrough")));
});
t.row(|r| {
r.cell(|ui| ui.checkbox(sup, tl!("Superscript")));
r.cell(|ui| ui.checkbox(sub, tl!("Subscript")));
});
t.row(|r| {
r.cell(|ui| ui.checkbox(small_caps, tl!("Small caps")));
r.cell(|ui| ui.checkbox(caps, tl!("All caps")));
});
t.row(|r| {
r.cell(|ui| ui.checkbox(hidden, tl!("Hidden")));
});
egui::Grid::new("fx").num_columns(2).show(ui, |ui| {
ui.checkbox(bold, tl!("Bold"));
ui.checkbox(italic, tl!("Italic"));
ui.end_row();
ui.checkbox(underline, tl!("Underline"));
ui.checkbox(strike, tl!("Strikethrough"));
ui.end_row();
ui.checkbox(sup, tl!("Superscript"));
ui.checkbox(sub, tl!("Subscript"));
ui.end_row();
ui.checkbox(small_caps, tl!("Small caps"));
ui.checkbox(caps, tl!("All caps"));
ui.end_row();
ui.checkbox(hidden, tl!("Hidden"));
ui.end_row();
});
// Live preview of the chosen font/size/colour/effects.
ui.separator();
@@ -314,31 +286,27 @@ fn paragraph_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ui.label(egui::RichText::new(tl!("Indentation")).font(semibold(12.5)));
// Mirrored indents are the inside and outside ones (they swap on even pages).
let (l_label, r_label) = if opts[0] { ("Inside:", "Outside:") } else { ("Left:", "Right:") };
crate::rtl::Table::new("ind").columns(4).show(ui, |t| {
t.row(|r| {
let left_label = r.cell(|ui| ui.label(tl!(l_label)));
r.cell(|ui| para_number(ui, left, ParaNumber::Length, "left", mixed, &left_label));
let right_label = r.cell(|ui| ui.label(tl!(r_label)));
r.cell(|ui| para_number(ui, right, ParaNumber::Length, "right", mixed, &right_label));
});
t.row(|r| {
let first_label = r.cell(|ui| ui.label(tl!("First line:")));
r.cell(|ui| para_number(ui, first, ParaNumber::Length, "indentFirst", mixed, &first_label));
});
egui::Grid::new("ind").num_columns(4).show(ui, |ui| {
let left_label = ui.label(tl!(l_label));
para_number(ui, left, ParaNumber::Length, "left", mixed, &left_label);
let right_label = ui.label(tl!(r_label));
para_number(ui, right, ParaNumber::Length, "right", mixed, &right_label);
ui.end_row();
let first_label = ui.label(tl!("First line:"));
para_number(ui, first, ParaNumber::Length, "indentFirst", mixed, &first_label);
ui.end_row();
});
para_check(ui, &mut opts[0], tl!("Mirror indents"), "mirrorIndents", mixed);
ui.label(egui::RichText::new(tl!("Spacing")).font(semibold(12.5)));
crate::rtl::Table::new("sp").columns(4).show(ui, |t| {
t.row(|r| {
let before_label = r.cell(|ui| ui.label(tl!("Before:")));
r.cell(|ui| para_number(ui, before, ParaNumber::Points, "spaceBefore", mixed, &before_label));
let line_label = r.cell(|ui| ui.label(tl!("Line spacing:")));
r.cell(|ui| para_number(ui, line, ParaNumber::Lines, "lineSpacing", mixed, &line_label));
});
t.row(|r| {
let after_label = r.cell(|ui| ui.label(tl!("After:")));
r.cell(|ui| para_number(ui, after, ParaNumber::Points, "spaceAfter", mixed, &after_label));
});
egui::Grid::new("sp").num_columns(4).show(ui, |ui| {
let before_label = ui.label(tl!("Before:"));
para_number(ui, before, ParaNumber::Points, "spaceBefore", mixed, &before_label);
let line_label = ui.label(tl!("Line spacing:"));
para_number(ui, line, ParaNumber::Lines, "lineSpacing", mixed, &line_label);
ui.end_row();
let after_label = ui.label(tl!("After:"));
para_number(ui, after, ParaNumber::Points, "spaceAfter", mixed, &after_label);
ui.end_row();
});
para_check(ui, &mut opts[1], tl!("Don't add space between paragraphs of the same style"), "contextualSpacing", mixed);
}
@@ -419,8 +387,8 @@ fn find_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
// Word greys out Match case and the word options with wildcards (they always match case).
let plain = !extra.wildcards;
ui.horizontal(|ui| {
ui.add_enabled_ui(plain, |ui| ui.checkbox(match_case, tl!("Match case")));
ui.add_enabled_ui(plain, |ui| ui.checkbox(whole_word, tl!("Whole words")));
ui.add_enabled(plain, egui::Checkbox::new(match_case, tl!("Match case")));
ui.add_enabled(plain, egui::Checkbox::new(whole_word, tl!("Whole words")));
ui.checkbox(&mut extra.wildcards, tl!("Use wildcards"));
});
let arrow = if extra.more { "▾" } else { "▸" };
@@ -429,12 +397,12 @@ fn find_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
if extra.more {
ui.horizontal(|ui| {
ui.add_enabled_ui(plain, |ui| ui.checkbox(&mut extra.prefix, tl!("Match prefix")));
ui.add_enabled_ui(plain, |ui| ui.checkbox(&mut extra.suffix, tl!("Match suffix")));
ui.add_enabled(plain, egui::Checkbox::new(&mut extra.prefix, tl!("Match prefix")));
ui.add_enabled(plain, egui::Checkbox::new(&mut extra.suffix, tl!("Match suffix")));
});
ui.horizontal(|ui| {
ui.add_enabled_ui(plain, |ui| ui.checkbox(&mut extra.ignore_punct, tl!("Ignore punctuation characters")));
ui.add_enabled_ui(plain, |ui| ui.checkbox(&mut extra.ignore_space, tl!("Ignore white-space characters")));
ui.add_enabled(plain, egui::Checkbox::new(&mut extra.ignore_punct, tl!("Ignore punctuation characters")));
ui.add_enabled(plain, egui::Checkbox::new(&mut extra.ignore_space, tl!("Ignore white-space characters")));
});
ui.horizontal(|ui| {
ui.checkbox(regex, tl!("Regular expressions"));
@@ -510,15 +478,15 @@ fn enclose_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
});
ui.add_space(6.0);
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(tl!("Text:"));
ui.add(egui::TextEdit::singleline(text).desired_width(110.0).char_limit(2));
ui.add_space(6.0);
enclose_preview(ui, style, text, shape);
});
ui.add_space(12.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.label(tl!("Enclosure"));
for (v, l) in [("circle", "Circle"), ("square", "Square"), ("triangle", "Triangle"), ("diamond", "Diamond")] {
ui.add_enabled_ui(style != "none", |ui| ui.selectable_value(shape, v.to_string(), tl!(l)));
@@ -567,15 +535,13 @@ fn insert_table_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let Dialog::InsertTable { rows, cols } = d else {
return false;
};
crate::rtl::Table::new("it").columns(2).show(ui, |t| {
t.row(|r| {
r.cell(|ui| ui.label(tl!("Number of columns:")));
r.cell(|ui| ui.add(egui::DragValue::new(cols).range(1..=63)));
});
t.row(|r| {
r.cell(|ui| ui.label(tl!("Number of rows:")));
r.cell(|ui| ui.add(egui::DragValue::new(rows).range(1..=1000)));
});
egui::Grid::new("it").num_columns(2).show(ui, |ui| {
ui.label(tl!("Number of columns:"));
ui.add(egui::DragValue::new(cols).range(1..=63));
ui.end_row();
ui.label(tl!("Number of rows:"));
ui.add(egui::DragValue::new(rows).range(1..=1000));
ui.end_row();
});
let (ok, cancel) = buttons(ui, tl!("OK"));
if ok {
@@ -584,11 +550,6 @@ fn insert_table_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ok || cancel
}
/// A margin's number box, in the unit the interface measures in.
fn margin(value: &mut f32) -> egui::DragValue<'_> {
egui::DragValue::new(value).speed(3.6 / crate::options::unit().pt_per_unit()).measure(crate::options::unit()).max_decimals(2)
}
fn page_setup_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let Dialog::PageSetup {
top,
@@ -612,19 +573,21 @@ fn page_setup_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
return false;
};
ui.label(egui::RichText::new(tl!("Margins")).font(semibold(12.5)));
crate::rtl::Table::new("ps").columns(4).show(ui, |t| {
t.row(|r| {
let top_label = r.cell(|ui| ui.label(tl!("Top:")));
r.cell(|ui| ui.add(margin(top)).labelled_by(top_label.id));
let bottom_label = r.cell(|ui| ui.label(tl!("Bottom:")));
r.cell(|ui| ui.add(margin(bottom)).labelled_by(bottom_label.id));
});
t.row(|r| {
let left_label = r.cell(|ui| ui.label(if *mirror { tl!("Inside:") } else { tl!("Left:") }));
r.cell(|ui| ui.add(margin(left)).labelled_by(left_label.id));
let right_label = r.cell(|ui| ui.label(if *mirror { tl!("Outside:") } else { tl!("Right:") }));
r.cell(|ui| ui.add(margin(right)).labelled_by(right_label.id));
});
egui::Grid::new("ps").num_columns(4).show(ui, |ui| {
let top_label = ui.label(tl!("Top:"));
ui.add(egui::DragValue::new(top).speed(3.6 / crate::options::unit().pt_per_unit()).measure(crate::options::unit()).max_decimals(2))
.labelled_by(top_label.id);
let bottom_label = ui.label(tl!("Bottom:"));
ui.add(egui::DragValue::new(bottom).speed(3.6 / crate::options::unit().pt_per_unit()).measure(crate::options::unit()).max_decimals(2))
.labelled_by(bottom_label.id);
ui.end_row();
let left_label = ui.label(if *mirror { tl!("Inside:") } else { tl!("Left:") });
ui.add(egui::DragValue::new(left).speed(3.6 / crate::options::unit().pt_per_unit()).measure(crate::options::unit()).max_decimals(2))
.labelled_by(left_label.id);
let right_label = ui.label(if *mirror { tl!("Outside:") } else { tl!("Right:") });
ui.add(egui::DragValue::new(right).speed(3.6 / crate::options::unit().pt_per_unit()).measure(crate::options::unit()).max_decimals(2))
.labelled_by(right_label.id);
ui.end_row();
});
ui.label(egui::RichText::new(tl!("Orientation")).font(semibold(12.5)));
ui.horizontal(|ui| {
@@ -641,13 +604,12 @@ fn page_setup_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ui.radio_value(&mut grid.0, 2, tl!("Lines and characters"));
ui.radio_value(&mut grid.0, 3, tl!("Text snaps to characters"));
});
crate::rtl::Table::new("ps_grid").columns(4).show(ui, |t| {
t.row(|r| {
let chars_label = r.cell(|ui| ui.add_enabled(grid.0 >= 2, egui::Label::new(tl!("Characters per line:"))));
r.cell(|ui| ui.add_enabled(grid.0 >= 2, egui::DragValue::new(&mut grid.2).range(1..=1000)).labelled_by(chars_label.id));
let lines_label = r.cell(|ui| ui.add_enabled(grid.0 >= 1, egui::Label::new(tl!("Lines per page:"))));
r.cell(|ui| ui.add_enabled(grid.0 >= 1, egui::DragValue::new(&mut grid.1).range(1..=1000)).labelled_by(lines_label.id));
});
egui::Grid::new("ps_grid").num_columns(4).show(ui, |ui| {
let chars_label = ui.add_enabled(grid.0 >= 2, egui::Label::new(tl!("Characters per line:")));
ui.add_enabled(grid.0 >= 2, egui::DragValue::new(&mut grid.2).range(1..=1000)).labelled_by(chars_label.id);
let lines_label = ui.add_enabled(grid.0 >= 1, egui::Label::new(tl!("Lines per page:")));
ui.add_enabled(grid.0 >= 1, egui::DragValue::new(&mut grid.1).range(1..=1000)).labelled_by(lines_label.id);
ui.end_row();
});
ui.horizontal(|ui| {
ui.label(tl!("Section direction:"));
@@ -718,7 +680,7 @@ fn link_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
});
ui.add_space(4.0);
crate::rtl::grid(ui, egui::Grid::new("lnk").num_columns(2), |ui| {
egui::Grid::new("lnk").num_columns(2).show(ui, |ui| {
ui.label(tl!("Text to display:"));
ui.text_edit_singleline(text);
ui.end_row();
@@ -800,7 +762,7 @@ fn word_count_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let Dialog::WordCount { stats } = d else {
return false;
};
crate::rtl::grid(ui, egui::Grid::new("wc").num_columns(2).spacing(vec2(30.0, 6.0)), |ui| {
egui::Grid::new("wc").num_columns(2).spacing(vec2(30.0, 6.0)).show(ui, |ui| {
for (l, k) in [
("Pages", "pages"),
("Words", "words"),
@@ -893,7 +855,7 @@ fn phonetic_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
("right", "Right"),
("rightVertical", "Right (vertical)"),
];
crate::rtl::grid(ui, egui::Grid::new("phonetic").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("phonetic").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Base text:"));
ui.label(egui::RichText::new(base.as_str()).strong());
ui.end_row();
@@ -938,7 +900,7 @@ fn new_style_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let Dialog::NewStyle { name, based_on, back } = d else {
return false;
};
crate::rtl::grid(ui, egui::Grid::new("ns").num_columns(2), |ui| {
egui::Grid::new("ns").num_columns(2).show(ui, |ui| {
ui.label(tl!("Name:"));
ui.text_edit_singleline(name);
ui.end_row();
@@ -962,7 +924,7 @@ fn modify_style_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let Dialog::ModifyStyle { back, resume, id, name, font, size, bold, italic, color, before, after } = d else {
return false;
};
crate::rtl::grid(ui, egui::Grid::new("ms").num_columns(2), |ui| {
egui::Grid::new("ms").num_columns(2).show(ui, |ui| {
let name_label = ui.label(tl!("Name:"));
ui.text_edit_singleline(name).labelled_by(name_label.id);
ui.end_row();
@@ -1182,7 +1144,7 @@ fn save_changes_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
ui.add_space(8.0);
let buttons = ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let cancel = ui.button(tl!("Cancel"));
let dont_save = ui.button(tl!("Don't Save"));
let save = ui.add(crate::widgets::primary_button(ui, tl!("Save")));
@@ -1292,7 +1254,7 @@ fn encrypt_password_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bo
};
ui.label(tl!("Anyone who opens the document will need this password."));
ui.add_space(4.0);
crate::rtl::grid(ui, egui::Grid::new("encrypt_password").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("encrypt_password").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Password:"));
let r = ui.add(egui::TextEdit::singleline(password.as_mut_string()).password(true).desired_width(220.0));
if password.is_empty() && confirm.is_empty() && !r.has_focus() {
@@ -1475,7 +1437,7 @@ fn merge_rule_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
"Insert one text or another, depending on a field's value."
}));
ui.add_space(4.0);
crate::rtl::grid(ui, egui::Grid::new("merge_rule").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("merge_rule").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Field:"));
if fields.is_empty() {
ui.text_edit_singleline(field);
@@ -1517,7 +1479,7 @@ fn match_fields_dialog(ui: &mut Ui, d: &mut Dialog) -> bool {
} else {
ui.label(tl!("The recipient-list field that fills each part of an address block and greeting line."));
ui.add_space(4.0);
crate::rtl::grid(ui, egui::Grid::new("match_fields").num_columns(2).spacing(vec2(24.0, 6.0)).striped(true), |ui| {
egui::Grid::new("match_fields").num_columns(2).spacing(vec2(24.0, 6.0)).striped(true).show(ui, |ui| {
for (part, key) in ADDRESS_PARTS {
ui.label(tl!(part));
match address.get(key).and_then(Value::as_str) {
+3 -3
View File
@@ -177,8 +177,8 @@ pub(super) fn goto_body(app: &mut WordApp, ui: &mut Ui, what: &mut GoToWhat, tar
} else {
Vec::new()
};
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.label(tl!("Go to what:"));
for (w, label) in [(GoToWhat::Page, "Page"), (GoToWhat::Bookmark, "Bookmark")] {
if ui.selectable_label(*what == w, tl!(label)).clicked() && *what != w {
@@ -189,7 +189,7 @@ pub(super) fn goto_body(app: &mut WordApp, ui: &mut Ui, what: &mut GoToWhat, tar
}
});
ui.add_space(16.0);
crate::rtl::vertical(ui, |ui| match what {
ui.vertical(|ui| match what {
GoToWhat::Page => {
let label = ui.label(tl!("Enter page number:"));
let r = ui.add(egui::TextEdit::singleline(target).desired_width(220.0)).labelled_by(label.id);
+7 -7
View File
@@ -343,7 +343,7 @@ pub fn table_grid_picker(ui: &mut Ui, app: &mut WordApp) {
});
let mut new_hover = (0, 0);
let mut clicked = None;
crate::rtl::grid(ui, egui::Grid::new("tgp").spacing(vec2(2.0, 2.0)), |ui| {
egui::Grid::new("tgp").spacing(vec2(2.0, 2.0)).show(ui, |ui| {
for r in 1..=8 {
for c in 1..=10 {
let (rect, resp) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::click());
@@ -390,7 +390,7 @@ pub(super) fn table_properties(ui: &mut Ui, f: &mut TableForm) {
ui.label(egui::RichText::new(tl!(s)).font(semibold(12.5)));
};
heading(ui, "Table");
crate::rtl::grid(ui, egui::Grid::new("tp_table").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("tp_table").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.checkbox(&mut f.width_on, tl!("Preferred width:"));
ui.add_enabled(f.width_on, len(&mut f.width, 0.1, unit));
ui.end_row();
@@ -412,7 +412,7 @@ pub(super) fn table_properties(ui: &mut Ui, f: &mut TableForm) {
ui.end_row();
});
heading(ui, "Row");
crate::rtl::grid(ui, egui::Grid::new("tp_row").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("tp_row").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let specify = ui.checkbox(&mut f.row_height_on, tl!("Specify height:"));
ui.horizontal(|ui| {
ui.add_enabled(f.row_height_on, len(&mut f.row_height, 0.02, unit));
@@ -433,13 +433,13 @@ pub(super) fn table_properties(ui: &mut Ui, f: &mut TableForm) {
super::paragraph::para_check(ui, &mut f.allow_break, tl!("Allow row to break across pages"), "allowBreak", &mut f.mixed);
super::paragraph::para_check(ui, &mut f.header_row, tl!("Repeat as header row on each page"), "headerRow", &mut f.mixed);
heading(ui, "Column");
crate::rtl::grid(ui, egui::Grid::new("tp_col").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("tp_col").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Preferred width:"));
ui.add(len(&mut f.column_width, 0.1, unit));
ui.end_row();
});
heading(ui, "Cell");
crate::rtl::grid(ui, egui::Grid::new("tp_cell").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("tp_cell").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.checkbox(&mut f.cell_width_on, tl!("Preferred width:"));
ui.add_enabled(f.cell_width_on, len(&mut f.cell_width, 0.1, unit));
ui.end_row();
@@ -456,7 +456,7 @@ pub(super) fn table_properties(ui: &mut Ui, f: &mut TableForm) {
pub(super) fn cell_margins_dialog(ui: &mut Ui, f: &mut CellMarginsForm) {
let unit = crate::options::unit();
let max = 144.0 / unit.pt_per_unit();
crate::rtl::grid(ui, egui::Grid::new("cell_margins").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("cell_margins").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
for (label, value) in [("Top:", &mut f.top), ("Left:", &mut f.left), ("Bottom:", &mut f.bottom), ("Right:", &mut f.right)] {
ui.label(tl!(label));
ui.add(egui::DragValue::new(value).speed(0.01).range(0.0..=max).measure(unit).max_decimals(2));
@@ -512,7 +512,7 @@ pub(super) fn custom_properties(app: &mut WordApp, ui: &mut Ui, f: &mut CustomPr
}
});
ui.separator();
crate::rtl::grid(ui, egui::Grid::new("custom_props_form").num_columns(2).spacing(vec2(10.0, 6.0)), |ui| {
egui::Grid::new("custom_props_form").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
let name = ui.label(tl!("Name:"));
ui.add(egui::TextEdit::singleline(&mut f.name).desired_width(300.0).char_limit(wordcraft_engine::cmd::file::MAX_CUSTOM_NAME))
.labelled_by(name.id);
+5 -4
View File
@@ -261,7 +261,8 @@ pub(crate) fn recipient_list(
if !editing {
filter.editing = None;
}
ui.add_enabled_ui(order.is_some(), |ui| ui.checkbox(&mut filter.only_merged, tl!("Show only records that will be merged")));
let merged_only = egui::Checkbox::new(&mut filter.only_merged, tl!("Show only records that will be merged"));
ui.add_enabled(order.is_some(), merged_only);
let visible = ui
.horizontal(|ui| {
ui.label(tl!("Find in list:"));
@@ -283,7 +284,7 @@ pub(crate) fn recipient_list(
let mut x_w = X_W;
let mut focused = None;
egui::ScrollArea::horizontal().id_salt("recipients_h").max_width(720.0).show(ui, |ui| {
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.allocate_exact_size(vec2(NUM_W, ROW_H - 4.0), Sense::hover());
for (i, f) in fields.iter_mut().enumerate() {
@@ -423,7 +424,7 @@ fn recipient_filter(ui: &mut Ui, fields: &[String], filter: &mut RecipientFilter
});
};
let mut remove = None;
crate::rtl::grid(ui, egui::Grid::new("recipient_filter").num_columns(5).spacing(vec2(6.0, 4.0)), |ui| {
egui::Grid::new("recipient_filter").num_columns(5).spacing(vec2(6.0, 4.0)).show(ui, |ui| {
ui.label("");
ui.label(egui::RichText::new(tl!("Field")).weak());
ui.label(egui::RichText::new(tl!("Comparison")).weak());
@@ -465,7 +466,7 @@ fn recipient_filter(ui: &mut Ui, fields: &[String], filter: &mut RecipientFilter
}
});
ui.add_space(4.0);
crate::rtl::grid(ui, egui::Grid::new("recipient_sort").num_columns(3).spacing(vec2(6.0, 4.0)), |ui| {
egui::Grid::new("recipient_sort").num_columns(3).spacing(vec2(6.0, 4.0)).show(ui, |ui| {
for (i, (field, desc)) in filter.sort.iter_mut().enumerate() {
ui.label(tl!(if i == 0 { "Sort by" } else { "Then by" }));
field_combo(ui, ("rs_field", i), field, true);
+11 -15
View File
@@ -106,23 +106,19 @@ pub(super) fn page_paper_group(ui: &mut Ui, paper: &mut (f32, f32), landscape: &
});
combo.response.labelled_by(label.id);
});
crate::rtl::Table::new("ps_paper").columns(4).show(ui, |t| {
egui::Grid::new("ps_paper").num_columns(4).show(ui, |ui| {
use wordcraft_engine::cmd::page::{MAX_PAGE_SIDE, MIN_PAGE_SIDE};
let range = MIN_PAGE_SIDE / ppu..=MAX_PAGE_SIDE / ppu;
t.row(|r| {
let width_label = r.cell(|ui| ui.label(tl!(l.width)));
let w = r.cell(|ui| {
ui.add(egui::DragValue::new(&mut paper.0).speed(3.6 / ppu).range(range.clone()).measure(unit).max_decimals(2))
.labelled_by(width_label.id)
});
let height_label = r.cell(|ui| ui.label(tl!(l.height)));
let h = r.cell(|ui| {
ui.add(egui::DragValue::new(&mut paper.1).speed(3.6 / ppu).range(range).measure(unit).max_decimals(2)).labelled_by(height_label.id)
});
if (w.changed() || h.changed()) && paper.0 != paper.1 {
*landscape = paper.0 > paper.1;
}
});
let width_label = ui.label(tl!(l.width));
let w = ui
.add(egui::DragValue::new(&mut paper.0).speed(3.6 / ppu).range(range.clone()).measure(unit).max_decimals(2))
.labelled_by(width_label.id);
let height_label = ui.label(tl!(l.height));
let h = ui.add(egui::DragValue::new(&mut paper.1).speed(3.6 / ppu).range(range).measure(unit).max_decimals(2)).labelled_by(height_label.id);
if (w.changed() || h.changed()) && paper.0 != paper.1 {
*landscape = paper.0 > paper.1;
}
ui.end_row();
});
}
-6
View File
@@ -61,9 +61,6 @@ pub(super) enum ParaNumber {
Lines,
}
/// The right-to-left mark.
const RLM: char = '\u{200F}';
/// A Paragraph dialog number box for the field `key`. While the selected paragraphs differ in
/// it, it shows nothing, not even the unit, until a value is typed or dragged in.
pub(super) fn para_number(ui: &mut Ui, value: &mut f32, kind: ParaNumber, key: &str, mixed: &mut BTreeSet<String>, label: &egui::Response) {
@@ -79,9 +76,6 @@ pub(super) fn para_number(ui: &mut Ui, value: &mut f32, kind: ParaNumber, key: &
ParaNumber::Length if blank => field.measure_blank(unit),
ParaNumber::Length => field.measure(unit),
_ if blank => field.custom_formatter(|_, _| String::new()),
// Right to left, the unit's space goes after the word and a right-to-left mark closes it: a
// plain leading space between the digits and an Arabic unit is drawn with no width.
ParaNumber::Points if crate::rtl::on() => field.suffix(format!("{} {RLM}", tl!("pt"))),
ParaNumber::Points => field.suffix(format!(" {}", tl!("pt"))),
ParaNumber::Lines => field,
};
+9 -9
View File
@@ -124,7 +124,7 @@ fn math_menu_button(ui: &mut Ui, app: &mut WordApp, picture: &str, label: &str,
y += LABEL_PITCH;
}
let resp = resp.on_hover_text(label.replace('\n', " "));
crate::rtl::menu(&resp).show(|ui| {
egui::Popup::menu(&resp).show(|ui| {
ui.set_min_width(260.0);
menu(ui, app);
});
@@ -239,7 +239,7 @@ pub fn insert_button(ui: &mut Ui, app: &mut WordApp) {
let _ = app.run("insert.equation", json!({}));
app.canvas.want_focus = true;
}
crate::rtl::menu(&rb).show(|ui| builtin_menu(ui, app));
egui::Popup::menu(&rb).show(|ui| builtin_menu(ui, app));
}
/// The Symbols group: a strip of the current set's symbols, and a menu with every set.
@@ -253,7 +253,7 @@ fn symbols(ui: &mut Ui, app: &mut WordApp) {
let all: Vec<char> = chars.chars().collect();
let start = ui.data(|d| d.get_temp::<usize>(start_id)).unwrap_or(0).min(all.len().saturating_sub(1));
let t = Tokens::get(ui.ctx());
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(1.0, 1.0);
for row in 0..rows {
ui.horizontal(|ui| {
@@ -272,7 +272,7 @@ fn symbols(ui: &mut Ui, app: &mut WordApp) {
});
}
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(0.0, 1.0);
let page = per_row * rows;
if ui.add_enabled(start > 0, egui::Button::new("▲").min_size(vec2(16.0, 22.0))).clicked() {
@@ -282,7 +282,7 @@ fn symbols(ui: &mut Ui, app: &mut WordApp) {
ui.data_mut(|d| d.insert_temp(start_id, start + per_row));
}
let resp = ui.add(egui::Button::new("…").min_size(vec2(16.0, 22.0))).on_hover_text(tl!("All symbol sets"));
crate::rtl::menu(&resp).show(|ui| {
egui::Popup::menu(&resp).show(|ui| {
ui.set_width(420.0);
ui.horizontal_wrapped(|ui| {
for (i, (name, _)) in SYMBOL_SETS.iter().enumerate() {
@@ -341,7 +341,7 @@ fn symbol_tooltip(c: char, display: &str) -> String {
pub fn show(app: &mut WordApp, ui: &mut Ui) {
group(ui, "Tools", None, app, |ui, app| {
math_menu_button(ui, app, EQUATION_PICTURE, "Equation", builtin_menu);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(1.0, 0.0);
let latex = app.session.math_latex;
small(
@@ -386,7 +386,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
},
);
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(1.0, 0.0);
let normal = app.session.math_normal_text;
small(
@@ -407,7 +407,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
}
});
group(ui, "Equation Options", None, app, |ui, app| {
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(1.0, 0.0);
let info = app.session.run("equation.get", &json!({})).ok();
let display = info.as_ref().and_then(|v| v.get("display")).and_then(|v| v.as_bool()).unwrap_or(false);
@@ -437,7 +437,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
crate::widgets::ButtonAction::new("equation.number", json!({}), numbered),
);
});
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(1.0, 0.0);
crate::widgets::menu_button(
ui,
+4 -16
View File
@@ -316,7 +316,7 @@ pub(crate) fn show(app: &mut WordApp, ctx: &egui::Context) {
.anchor(Align2::CENTER_CENTER, vec2(0.0, -20.0));
let shown = crate::window_close::window(ctx, window).show(ctx, |ui| {
ui.set_width(620.0);
done = crate::rtl::root(ui, |ui| body(ui, &mut b, &t));
done = body(ui, &mut b, &t);
});
if shown.is_some_and(|s| crate::window_close::show(ctx, &s.response, &t)) {
done = Some(None);
@@ -385,21 +385,9 @@ fn body(ui: &mut Ui, b: &mut FileBrowser, t: &Tokens) -> Option<Option<PathBuf>>
} else if resp.hovered() {
ui.painter().rect_filled(r, 2.0, t.hover);
}
let rtl = crate::rtl::on();
let icon = Rect::from_center_size(pos2(crate::rtl::lead_x(rtl, r, 14.0), r.center().y), vec2(18.0, 18.0));
let icon = Rect::from_center_size(pos2(r.min.x + 14.0, r.center().y), vec2(18.0, 18.0));
icons::paint(ui.painter(), icon, if e.dir { "folder" } else { "file" }, t.icon, t.accent);
// A long name keeps its beginning and ends in an ellipsis inside the row, from the
// side reading starts from; the whole name is the row's tooltip then.
let mut job = egui::text::LayoutJob::simple_singleline(crate::i18n::isolate(&e.name), regular(13.0), t.text);
job.wrap.max_width = (r.width() - 36.0).max(0.0);
job.wrap.max_rows = 1;
job.wrap.break_anywhere = true;
let galley = ui.painter().layout_job(job);
let text_x = crate::rtl::lead_x(rtl, r, 30.0);
let at = pos2(if rtl { text_x - galley.size().x } else { text_x }, r.center().y - galley.size().y / 2.0);
let elided = galley.elided;
ui.painter().galley(at, galley, t.text);
let resp = if elided { resp.on_hover_text(&e.name) } else { resp };
ui.painter().with_clip_rect(r).text(pos2(r.min.x + 30.0, r.center().y), Align2::LEFT_CENTER, &e.name, regular(13.0), t.text);
if resp.double_clicked() {
if e.dir {
go = Some(e.path.clone());
@@ -471,7 +459,7 @@ fn body(ui: &mut Ui, b: &mut FileBrowser, t: &Tokens) -> Option<Option<PathBuf>>
let mut cancel = ui.input(|i| i.key_pressed(egui::Key::Escape));
let enter = !location_done && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.horizontal(|ui| {
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (ok, cancelb) =
crate::dialogs::ok_cancel_responses(ui, |ui| ui.add(crate::widgets::primary_button(ui, tl!(b.ok_label()))), tl!("Cancel"));
cancel |= cancelb.clicked();
+2 -2
View File
@@ -301,7 +301,7 @@ impl WordApp {
self.run("file.saveAs", json!({"path": path}))
};
if r.is_ok() {
self.status(crate::i18n::fmt(tl!("Exported {path}"), &[("path", &crate::i18n::isolate_path(&path))]));
self.status(crate::i18n::fmt(tl!("Exported {path}"), &[("path", &path)]));
}
r
}
@@ -311,7 +311,7 @@ impl WordApp {
params["path"] = json!(path);
let r = self.run("file.printPdf", params);
if r.is_ok() {
self.status(crate::i18n::fmt(tl!("Exported {path}"), &[("path", &crate::i18n::isolate_path(&path))]));
self.status(crate::i18n::fmt(tl!("Exported {path}"), &[("path", &path)]));
}
r
}
+3 -3
View File
@@ -216,7 +216,7 @@ fn kind_of(t: usize, focus: [f32; 4], angle: f32, scaled: bool) -> GradientKind
/// add and remove, and the selected stop's colour, position, transparency and rotate-with-shape
/// controls.
fn gradient(app: &mut WordApp, ui: &mut Ui, theme: &[Rgb], g: &Gradient) {
crate::rtl::grid(ui, egui::Grid::new("format_shape.gradient").num_columns(2).spacing(vec2(8.0, 6.0)), |ui| {
egui::Grid::new("format_shape.gradient").num_columns(2).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Preset gradients"));
ui.menu_button("▾", |ui| {
if let Some(f) = presets_gallery(ui, theme) {
@@ -394,7 +394,7 @@ fn stops_bar(app: &mut WordApp, ui: &mut Ui, g: &Gradient, sel: usize) {
fn presets_gallery(ui: &mut Ui, theme: &[Rgb]) -> Option<Fill> {
let presets = shape_fill::presets(theme);
let mut out = None;
crate::rtl::grid(ui, egui::Grid::new("format_shape.presets").spacing(vec2(4.0, 4.0)), |ui| {
egui::Grid::new("format_shape.presets").spacing(vec2(4.0, 4.0)).show(ui, |ui| {
for (i, f) in presets.iter().enumerate() {
let style = shape_fill::PRESET_STYLES.get(i / 6).copied().unwrap_or("");
let name = format!("{}, {} {}", tl!(style), tl!("Accent"), i % 6 + 1);
@@ -418,7 +418,7 @@ fn directions(ui: &mut Ui, g: &Gradient, t: usize) -> Option<GradientKind> {
PATH_DIRECTIONS.iter().map(|&f| (kind_of(t, f, 90.0, true), shape_fill::path_direction_name(f))).collect()
};
let mut out = None;
crate::rtl::grid(ui, egui::Grid::new("format_shape.directions").spacing(vec2(4.0, 4.0)), |ui| {
egui::Grid::new("format_shape.directions").spacing(vec2(4.0, 4.0)).show(ui, |ui| {
for (i, (k, name)) in kinds.into_iter().enumerate() {
let f = with(g, |g| set_kind(g, k));
if swatch(ui, &f, &name) {
+3 -59
View File
@@ -70,7 +70,7 @@ pub struct LangInfo {
/// The languages WordCraft offers, when their catalogs are embedded. English first: it is the
/// fallback and the source language.
pub static LANGUAGES: [LangInfo; 28] = [
pub static LANGUAGES: [LangInfo; 27] = [
LangInfo { code: "en", prefer_hans: false, editing_lang: None },
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` resolve here (the crate's matching).
LangInfo { code: "zh-hans", prefer_hans: true, editing_lang: None },
@@ -124,18 +124,10 @@ pub static LANGUAGES: [LangInfo; 28] = [
LangInfo { code: "da", prefer_hans: false, editing_lang: None },
// Romanian; `ro`, `ro-RO`, `ro_RO.UTF-8` and `ro-MD` resolve here.
LangInfo { code: "ro", prefer_hans: false, editing_lang: None },
// Arabic (Modern Standard); `ar` and every `ar-*` locale (`ar-MA`, `ar-EG`, `ar-SA` …) resolve
// here. Right to left: see [`RTL_LANGUAGES`] and [`Lang::rtl`].
LangInfo { code: "ar", prefer_hans: false, editing_lang: None },
// Slovenian; `sl`, `sl-SI` and `sl_SI.UTF-8` resolve here.
LangInfo { code: "sl", prefer_hans: false, editing_lang: None },
];
/// Interface languages written right to left. Their catalogs are written in typing order and
/// egui's text layout orders each line itself, so the lookups return them as they are; the window
/// is mirrored ([`crate::rtl`]).
const RTL_LANGUAGES: [&str; 1] = ["ar"];
/// A language the UI can be shown in: English, or an offered language whose catalogs are embedded.
#[derive(Clone, Copy)]
pub struct Lang {
@@ -179,12 +171,6 @@ impl Lang {
self.i18n().name(self.lang)
}
/// Whether the interface is written right to left. Its translations come back from the lookups
/// as written, in typing order (see [`RTL_LANGUAGES`]).
pub fn rtl(self) -> bool {
RTL_LANGUAGES.contains(&self.code())
}
pub fn prefers_hans(self) -> bool {
self.info().is_some_and(|l| l.prefer_hans)
}
@@ -414,46 +400,6 @@ pub fn tr(lang: Lang, s: &str) -> &str {
lang.i18n().t(lang.lang, s)
}
/// Left-to-right mark: it keeps what it touches from joining a right-to-left run next to it.
const LRM: char = '\u{200E}';
/// `name` with a left-to-right mark before its extension (`ملف 2.docx`), which a right-to-left
/// line would otherwise draw as `ملف docx.2`.
fn mark_extension(name: &str) -> String {
match name.rfind('.') {
Some(dot)
if dot > 0 && name.get(dot + 1..).is_some_and(|e| !e.is_empty() && e.len() <= 8 && e.bytes().all(|b| b.is_ascii_alphanumeric())) =>
{
format!("{}{LRM}{}", &name[..dot], &name[dot..])
}
_ => name.to_string(),
}
}
/// `name` (a file or folder name, a document's name) set apart from the right-to-left text around
/// it, so its own direction and its neutral marks (`(`, `.`, `-`) keep their places. Only in a
/// right-to-left interface; elsewhere the text as it is. Display only: never for text that is saved.
pub fn isolate(name: &str) -> String {
if current().rtl() { format!("\u{2068}{}\u{2069}", mark_extension(name)) } else { name.to_string() }
}
/// [`isolate`] for a path or a web address, which always reads left to right: its folders keep
/// their order even where neighbouring ones are Arabic (a mark after every separator keeps two
/// right-to-left names from joining into one run that swaps them).
pub fn isolate_path(path: &str) -> String {
if !current().rtl() {
return path.to_string();
}
let mut out = String::with_capacity(path.len() + 8);
for c in mark_extension(path).chars() {
out.push(c);
if matches!(c, '/' | '\\') {
out.push(LRM);
}
}
format!("\u{2066}{out}\u{2069}")
}
/// A style's name as the interface shows it: built-in styles (`Normal`, `Heading 2`, `TOC 3`) in
/// the current language, styles the author made exactly as named. Display only: the document,
/// commands and saved files keep the English name.
@@ -479,11 +425,9 @@ pub fn builtin_style_name(name: &str) -> String {
name.to_string()
}
/// A ribbon location (`Home › Font`) in the current language, segment by segment. A
/// right-to-left language reads it from the right, so the mark points left.
/// A ribbon location (`Home › Font`) in the current language, segment by segment.
pub fn location(loc: &str) -> String {
let segments: Vec<&str> = loc.split(" › ").map(t).collect();
segments.join(if current().rtl() { " ‹ " } else { " › " })
loc.split(" › ").map(t).collect::<Vec<_>>().join(" › ")
}
/// A label with an action after a fixed English prefix, e.g. `Undo Typing`: both parts translated
-234
View File
@@ -597,237 +597,3 @@ fn disabled_reasons() -> std::collections::BTreeSet<String> {
}
reasons
}
/// One frame at a given window width; returns the left edge of every text drawn.
fn frame_at(ctx: &egui::Context, app: &mut WordApp, width: f32) -> Vec<(String, f32)> {
fn collect(shape: &egui::Shape, out: &mut Vec<(String, f32)>) {
match shape {
egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.pos.x)),
egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)),
_ => {}
}
}
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, 900.0));
let mut drawn = Vec::new();
for _ in 0..4 {
TIME.set(TIME.get() + 0.1);
let input = egui::RawInput { screen_rect: Some(screen), time: Some(TIME.get()), ..Default::default() };
let mut out = ctx.run_ui(input, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
drawn.clear();
for s in &out.shapes {
collect(&s.shape, &mut drawn);
}
out.textures_delta.clear();
}
drawn
}
/// The Arabic interface is mirrored: reading starts at the right. And nothing in it can panic:
/// every tab (wide, and narrow enough to scroll), File page, dialog, pane, context menu and
/// ribbon control is laid out right to left. egui's `Grid` asserts against right-to-left parents,
/// so a grid that is not wrapped in `rtl::grid` fails here.
#[test]
fn the_arabic_interface_is_mirrored_and_never_panics() {
// Arabic comes from the craft-i18n catalogs: a build without them (English only) has nothing to mirror.
let Some(ar) = super::Lang::from_code("ar") else {
eprintln!("skipped: built without the craft-i18n Arabic catalog (set CRAFT_I18N_DIR to a checkout)");
return;
};
let start = |ctx: &egui::Context| {
let mut app = fresh_app(ctx);
app.ui.language = "ar".into();
frame(ctx, &mut app);
app
};
let ctx = egui::Context::default();
let mut app = start(&ctx);
assert!(crate::rtl::on());
let x_of = |drawn: &[(String, f32)], english: &str| {
let label = super::tr(ar, english);
drawn.iter().find(|(t, _)| t == label).map(|(_, x)| *x).unwrap_or_else(|| panic!("{english} is not drawn"))
};
// Tabs run from the right: File, Home, Insert … and the Share button is at the far left.
let drawn = frame_at(&ctx, &mut app, 1600.0);
let (file, home, insert, share) = (x_of(&drawn, "File"), x_of(&drawn, "Home"), x_of(&drawn, "Insert"), x_of(&drawn, "Share"));
assert!(file > home && home > insert && insert > 800.0, "tabs: {file} {home} {insert}");
assert!(share < 200.0, "Share: {share}");
// Ribbon groups too: Clipboard is right of Font, which is right of Styles.
assert!(x_of(&drawn, "Clipboard") > x_of(&drawn, "Font") && x_of(&drawn, "Font") > x_of(&drawn, "Styles"));
// The status bar starts at the right, and a narrow window still shows the ribbon's first group.
let page = drawn.iter().find(|(t, _)| t.contains(super::tr(ar, "Editing"))).map(|(_, x)| *x);
assert!(page.is_some());
let narrow = frame_at(&ctx, &mut app, 700.0);
let clipboard = x_of(&narrow, "Clipboard");
assert!((350.0..700.0).contains(&clipboard), "the narrow ribbon starts at its right end: {clipboard}");
// Side panes swap sides: Navigation is on the right.
let _ = app.run("view.navigationPane", json!({"value": true}));
let drawn = frame_at(&ctx, &mut app, 1600.0);
assert!(x_of(&drawn, "Navigation") > 1300.0);
let _ = app.run("view.navigationPane", json!({"value": false}));
// English stays as it was.
{
let ctx = egui::Context::default();
let mut app = fresh_app(&ctx);
let drawn = frame_at(&ctx, &mut app, 1600.0);
let x = |label: &str| drawn.iter().find(|(t, _)| t == label).map(|(_, x)| *x).unwrap();
assert!(x("File") < x("Home") && x("Home") < x("Insert") && x("Insert") < 400.0 && x("Share") > 1400.0);
assert!(x("Clipboard") < x("Font") && x("Font") < x("Styles"));
assert!(x("Cut") < 200.0, "ribbon buttons keep their icon on the left and start at the left");
assert!(!crate::rtl::on());
super::set_current(ar);
}
// Everything else: no panic at any width.
let tabs: Vec<&str> = crate::ribbon::TABS.iter().copied().filter(|t| *t != "File").collect();
for tab in &tabs {
let _ = app.run("ui.tab", json!({"tab": tab}));
for width in [2560.0, 1100.0, 420.0] {
frame_at(&ctx, &mut app, width);
}
}
let _ = app.run("ui.tab", json!({"tab": "Home"}));
// Reading Mode's toolbar is mirrored too: its far-side button ("Edit Document") is at the left.
let _ = app.run("view.readMode", json!({}));
let drawn = frame_at(&ctx, &mut app, 1600.0);
let edit = x_of(&drawn, "Edit Document");
assert!(edit < 400.0, "Reading Mode: Edit Document at {edit}");
let _ = app.run("view.printLayout", json!({}));
// File › Print: the settings are on the right and the preview on the left, not over each other.
let _ = app.run("ui.backstage", json!({"value": true, "page": "print"}));
for width in [1600.0, 1100.0] {
let drawn = frame_at(&ctx, &mut app, width);
let settings = x_of(&drawn, "Save these pages as PDF…");
let indicator = super::tr(ar, "Page {page} of {pages}").split('{').next().unwrap_or_default();
// (The status bar's page count is drawn under the page too, at the right: take the leftmost.)
let preview = drawn.iter().filter(|(t, _)| t.starts_with(indicator)).map(|(_, x)| *x).fold(f32::MAX, f32::min);
assert!(preview < f32::MAX, "the preview's page indicator is drawn");
assert!(preview + 100.0 < settings, "print page at {width}: settings {settings}, preview {preview}");
}
for page in ["home", "new", "open", "info", "export", "print", "options"] {
let _ = app.run("ui.backstage", json!({"value": true, "page": page}));
frame(&ctx, &mut app);
frame_at(&ctx, &mut app, 500.0);
}
let _ = app.run("ui.backstage", json!({"value": false}));
// A dialog's columns start at the right too: Go To's list of targets is right of its input.
let _ = app.run("ui.dialog", json!({"name": "goto"}));
let drawn = frame_at(&ctx, &mut app, 1600.0);
let (targets, input) = (x_of(&drawn, "Go to what:"), x_of(&drawn, "Enter page number:"));
assert!(targets > input, "Go To: targets {targets}, input {input}");
app.dialog = None;
let dialogs = [
"font",
"paragraph",
"tabs",
"goto",
"insertTable",
"pageSetup",
"link",
"bookmark",
"wordCount",
"zoom",
"watermark",
"phonetic",
"newStyle",
"manageStyles",
"newTableStyle",
"modifyTableStyle",
"commands",
"icons",
"tableProperties",
"enclose",
"pasteSpecial",
"about",
"contributors",
"models",
"insertMergeField",
"findRecipient",
"fieldCode",
"encryptPassword",
"defineList",
"trackChangesOptions",
"customizeKeyboard",
"customizeRibbon",
"customizeQuickAccess",
"noteOptions",
"caption",
"index",
"markEntry",
"toc",
"sourceManager",
"createSource",
"lineNumbers",
"hyphenation",
"language",
"envelopes",
"labels",
"find",
"replace",
"symbol",
"field",
"columns",
"borders",
"pageBorders",
"newRecipientList",
"editRecipientList",
"rules",
"matchFields",
"dropCap",
"dateTime",
"crossReference",
];
for name in dialogs {
let _ = app.run("ui.dialog", json!({"name": name}));
frame(&ctx, &mut app);
app.dialog = None;
}
let panes: Vec<String> =
app.session.registry.all().iter().map(|c| c.id.to_string()).filter(|id| id.starts_with("view.") && id.ends_with("Pane")).collect();
for pane in &panes {
let _ = app.run(pane, json!({"value": true}));
frame(&ctx, &mut app);
frame_at(&ctx, &mut app, 600.0);
let _ = app.run(pane, json!({"value": false}));
}
frames(&ctx, &mut app, click(egui::pos2(1280.0, 520.0), egui::PointerButton::Secondary));
frames(&ctx, &mut app, escape());
let _ = app.run("insert.table", json!({"rows": 2, "cols": 2}));
for tab in ["Table Design", "Table Layout"] {
let _ = app.run("ui.tab", json!({"tab": tab}));
frame(&ctx, &mut app);
frame_at(&ctx, &mut app, 600.0);
}
let _ = app.run("insert.equation", json!({}));
let _ = app.run("ui.tab", json!({"tab": "Equation"}));
frame(&ctx, &mut app);
// Every ribbon control on every tab, hovered and clicked: its tooltip, menu or dialog.
let contextual = ["Table Design", "Table Layout", "Equation"];
let open = |tab: &str| {
let ctx = egui::Context::default();
let mut app = start(&ctx);
match tab {
"Table Design" | "Table Layout" => drop(app.run("insert.table", json!({"rows": 2, "cols": 2}))),
"Equation" => drop(app.run("insert.equation", json!({}))),
_ => {}
}
let _ = app.run("ui.tab", json!({"tab": tab}));
frame(&ctx, &mut app);
(ctx, app)
};
for tab in tabs.iter().chain(&contextual) {
let (ctx, mut app) = open(tab);
for at in ribbon_controls(&ctx) {
frames(&ctx, &mut app, vec![vec![egui::Event::PointerMoved(at)]; 2]);
frames(&ctx, &mut app, click(at, egui::PointerButton::Primary));
frames(&ctx, &mut app, escape());
app.dialog = None;
app.ui.backstage = false;
if app.ui.tab != *tab {
(_, app) = open(tab);
}
}
}
super::set_current(super::Lang::EN);
}
-110
View File
@@ -1189,113 +1189,3 @@ fn the_flatpak_manifest_pins_the_craft_i18n_rev() {
assert_eq!(commit.trim().trim_matches(['\'', '"']), rev);
assert!(manifest.contains("CRAFT_I18N_DIR: /run/build/wordcraft/craft-i18n"), "the build embeds the catalogs");
}
/// A right-to-left test language whose catalog is `source`, selected as the current language.
fn arabic_fixture(source: &'static str) -> Lang {
let ar = fixture_lang("ar", source);
set_current(ar);
ar
}
#[test]
fn arabic_locales_and_saved_preference_work_without_changing_the_document() {
needs_catalogs!();
let Some(ar) = Lang::from_code("ar") else {
eprintln!("skipped: the embedded catalogs have no Arabic");
return;
};
for tag in ["ar", "ar-MA", "ar_EG.UTF-8", "AR-sa", "ar_DZ@variant", "ar-Arab-AE"] {
assert_eq!(lang_from_tag(tag), Some(ar), "{tag}");
}
assert_eq!(first_supported(["xx-XX", "ar-MA", "en-US"]), Some(ar));
assert_eq!(normalize_pref("AR"), Some("ar"));
assert!(ar.rtl());
assert!(Lang::all().filter(|l| l.rtl()).eq([ar]), "Arabic is the only right-to-left language");
assert!(!ar.uses_cjk());
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_engine::sample::sample_document()), Default::default());
let before = serde_json::to_value(&app.session.doc).unwrap();
let result = app.run("ui.language", serde_json::json!({"value": "AR"})).unwrap();
assert_eq!(result["effective"], "ar");
assert_eq!(app.ui.language, "ar");
assert_eq!(serde_json::to_value(&app.session.doc).unwrap(), before);
let saved = serde_json::to_string(&app.ui).unwrap();
let restored: crate::UiState = serde_json::from_str(&saved).unwrap();
assert_eq!(Lang::from_pref(&restored.language), ar);
assert!(app.run("ui.language", serde_json::json!({"value": "ar-unknown"})).is_err());
assert_eq!(app.ui.language, "ar", "invalid preferences must keep the saved language");
}
/// Arabic comes back from the lookups exactly as the catalog has it, in typing order: egui's text
/// layout orders the words of each line itself, so nothing here reverses them. Placeholders are
/// filled in place and text for the document stays as typed.
#[test]
fn arabic_lookups_keep_typing_order() {
let ar = arabic_fixture(concat!(
"\tTable of Contents\tجدول المحتويات\n",
"\tPaste Special…\tلصق خاص…\n",
"\tAbout WordCraft\tحول WordCraft\n",
"\tPassword:\tكلمة المرور:\n",
"\tTable of\\nContents\tجدول\\nالمحتويات\n",
"\tPage {page} of {pages}\tالصفحة {page} من {pages}\n",
"\tExported {path}\tتم التصدير إلى {path}\n",
"preview\tTitle\tالعنوان\n",
"\tHome\tالرئيسية\n",
"\tFont\tالخط\n",
));
assert!(ar.rtl());
assert_eq!(tr(ar, "Table of Contents"), "جدول المحتويات");
assert_eq!(tr(ar, "Paste Special…"), "لصق خاص…");
assert_eq!(tr(ar, "About WordCraft"), "حول WordCraft");
assert_eq!(tr(ar, "Password:"), "كلمة المرور:");
// A two-line ribbon label keeps its two lines.
assert_eq!(tr(ar, "Table of\nContents"), "جدول\nالمحتويات");
// Placeholders are filled in place; inserted text is never touched, and the text for the
// document is the same string.
let page = fmt_plain(tr(ar, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]);
assert_eq!(page, "الصفحة 3 من 9");
assert_eq!(fmt_plain(tr(ar, "Exported {path}"), &[("path", "draft-{words}.docx")]), "تم التصدير إلى draft-{words}.docx");
// The preview context is looked up the same way.
assert_eq!(trc(ar, "preview", "Title"), "العنوان");
// A string without a translation stays English, untouched.
assert_eq!(tr(ar, "unknown future label"), "unknown future label");
set_current(ar);
// A ribbon location reads from the right: the tab first, the mark pointing left.
assert_eq!(location("Home › Font"), "الرئيسية ‹ الخط");
// A search query typed in reading order is a plain substring of the label.
assert!(lowercase(t("Table of Contents")).contains("جدول المحتويات"));
set_current(Lang::EN);
assert_eq!(location("Home › Font"), "Home › Font");
}
/// File names and paths shown in a right-to-left line are set apart and marked: the name in its
/// own isolate with its extension kept left to right, a path as a left-to-right isolate with a
/// mark after every separator so neighbouring Arabic folders do not swap. Other languages get
/// the text untouched.
#[test]
fn names_and_paths_are_isolated_in_a_right_to_left_interface() {
set_current(Lang::EN);
assert_eq!(isolate("تقرير 2.docx"), "تقرير 2.docx");
assert_eq!(isolate_path("C:/ملفات/تقرير.docx"), "C:/ملفات/تقرير.docx");
let ar = fixture_lang("ar", "\tHome\tالرئيسية\n");
set_current(ar);
assert_eq!(isolate("تقرير 2.docx"), "\u{2068}تقرير 2\u{200E}.docx\u{2069}");
assert_eq!(isolate("ملاحظات"), "\u{2068}ملاحظات\u{2069}", "no extension, no mark");
assert_eq!(isolate("v1.2 تقرير"), "\u{2068}v1.2 تقرير\u{2069}", "a dot that is not an extension");
assert_eq!(isolate_path(r"C:\ملفات\تقرير.docx"), "\u{2066}C:\\\u{200E}ملفات\\\u{200E}تقرير\u{200E}.docx\u{2069}");
assert_eq!(isolate_path("C:/ملفات/تقرير.docx"), "\u{2066}C:/\u{200E}ملفات/\u{200E}تقرير\u{200E}.docx\u{2069}");
set_current(Lang::EN);
}
/// A path inside a sentence (the open failure on the status bar) is isolated in a right-to-left
/// interface, so Arabic folders do not scramble; in English the message holds the path as it is.
#[test]
fn a_path_in_a_message_is_isolated_in_a_right_to_left_interface() {
let path = r"C:\ملفات\تقرير.docx";
set_current(Lang::EN);
let mut a = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default());
assert!(a.report_open_failure(path, "bad").contains(path));
set_current(fixture_lang("ar", "\tCouldn't open {path}: {error}\tتعذر فتح {path}: {error}\n"));
let msg = a.report_open_failure(path, "bad");
set_current(Lang::EN);
assert_eq!(msg, "تعذر فتح \u{2066}C:\\\u{200E}ملفات\\\u{200E}تقرير\u{200E}.docx\u{2069}: \u{2068}bad\u{2069}");
}
+1 -3
View File
@@ -337,8 +337,6 @@ fn licence_hover(pack: &Pack) -> String {
/// File › Options › Language › Language packs: every available and installed pack with its
/// name, size, licence and source, and Install or Remove.
pub(crate) fn options_section(app: &mut WordApp, ui: &mut Ui) {
// Pack names include Arabic and other scripts; ask for a system Arabic face when none is embedded.
app.want_system_arabic = true;
let t = Tokens::get(ui.ctx());
ui.label(RichText::new(tl!("Language packs")).font(semibold(15.0)));
ui.add_space(2.0);
@@ -409,7 +407,7 @@ pub(crate) fn options_section(app: &mut WordApp, ui: &mut Ui) {
let mut install = None;
let mut removal = None;
egui::ScrollArea::vertical().id_salt("options_language_packs").max_height(260.0).show(ui, |ui| {
crate::rtl::grid(ui, egui::Grid::new("options_language_packs_grid").num_columns(5).spacing(vec2(14.0, 6.0)).striped(true), |ui| {
egui::Grid::new("options_language_packs_grid").num_columns(5).spacing(vec2(14.0, 6.0)).striped(true).show(ui, |ui| {
for h in ["Language", "Size", "Licence", "Source", ""] {
ui.label(RichText::new(if h.is_empty() { "" } else { tl!(h) }).small().color(t.text_dim));
}
-8
View File
@@ -66,7 +66,6 @@ pub mod proofing_pane;
pub mod read_aloud;
pub mod ribbon;
pub mod ribbon_fit;
pub mod rtl;
pub mod ruler_tabs;
pub mod scroll;
pub mod system_clipboard;
@@ -533,16 +532,9 @@ pub struct WordApp {
fonts_hant: bool,
/// Whether the installed UI fonts include an installed CJK fallback font (#241).
fonts_system_cjk: bool,
/// Whether the installed UI fonts were asked to include an installed Arabic font (an Arabic
/// interface; used when no embedded face has Arabic letters).
fonts_system_arabic: bool,
/// CJK text is (about to be) on screen in a non-CJK interface, e.g. the language names in
/// File ▸ Options: load the installed CJK fallback font if no embedded face covers it.
pub(crate) want_system_cjk: bool,
/// Arabic text is (about to be) on screen in a non-Arabic interface, e.g. "العربية" in the
/// language picker or Language packs list: load an installed Arabic font when no embedded face
/// covers it (dev builds without craft-fonts).
pub(crate) want_system_arabic: bool,
/// The document (id, revision) whose comment text [`WordApp::ensure_comment_fonts`] last
/// looked at.
comment_fonts_for: Option<(u64, u64)>,
+1 -1
View File
@@ -354,7 +354,7 @@ pub(crate) fn highlight_button_appearance(name: &str) -> (&'static str, Option<e
/// No Color too, becomes the split button's colour, as in Word: after No Color a click on the
/// button removes highlighting (#2105).
pub(crate) fn highlight_menu(ui: &mut egui::Ui, app: &mut WordApp) {
crate::rtl::grid(ui, egui::Grid::new("mini_hl").spacing(Vec2::new(3.0, 3.0)), |ui| {
egui::Grid::new("mini_hl").spacing(Vec2::new(3.0, 3.0)).show(ui, |ui| {
for (i, h) in wordcraft_doc::props::Highlight::ALL.iter().enumerate().skip(1) {
let Some(c) = h.rgb() else { continue };
let (r, resp) = ui.allocate_exact_size(Vec2::new(18.0, 18.0), Sense::click());
+2 -4
View File
@@ -563,8 +563,7 @@ fn an_inline_picture_drags_to_the_insertion_cursor_and_stays_inline() {
/// page: its preview has its red outline and nothing dark inside.
#[test]
fn a_dragged_shape_leaves_the_text_behind_it() {
use crate::table_resize::tests::{H, button, gpu_turn, steps};
let _gpu = gpu_turn();
use crate::table_resize::tests::{H, button, steps};
let text = "MMMMMMMMMMMM ".repeat(12);
let mut a = WordApp::new(wordcraft_engine::Session::new(Document::from_text(&text)), Default::default());
let shape = InlineObject::Shape {
@@ -759,8 +758,7 @@ fn rotation_window(app: WordApp) -> crate::table_resize::tests::H {
/// pictures whose cached preview already contains a rotation and either reflection.
#[test]
fn rotation_drag_turns_picture_pixels_live_and_commits_once_or_cancels() {
use crate::table_resize::tests::{H, button, gpu_turn, steps};
let _gpu = gpu_turn();
use crate::table_resize::tests::{H, button, steps};
for (initial, flip_h, flip_v) in [(0.0, false, false), (30.0, true, false), (90.0, false, true), (315.0, true, true)] {
// Four original colour quadrants make both accidental extra rotation and reflected
// UVs observable. Sample near the long ends after a quarter turn, outside the old
+72 -111
View File
@@ -140,9 +140,8 @@ pub fn page(app: &mut WordApp, ui: &mut Ui) {
ui.label(RichText::new(tl!("Options")).font(semibold(26.0)));
ui.add_space(16.0);
let current = if PANES.contains(&app.ui.options_pane.as_str()) { app.ui.options_pane.clone() } else { "general".to_string() };
let rtl = crate::rtl::on();
let body = |ui: &mut Ui| {
crate::rtl::vertical(ui, |ui| {
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.set_width(170.0);
for id in PANES {
let label = tl!(pane_label(id));
@@ -156,13 +155,7 @@ pub fn page(app: &mut WordApp, ui: &mut Ui) {
}
let font = if active { semibold(14.0) } else { crate::theme::regular(14.0) };
// Long translations stay inside the row; WidgetInfo above keeps the full name.
ui.painter().with_clip_rect(r).text(
egui::pos2(crate::rtl::lead_x(rtl, r, 12.0), r.center().y),
crate::rtl::lead_center(rtl),
label,
font,
t.text,
);
ui.painter().with_clip_rect(r).text(egui::pos2(r.min.x + 12.0, r.center().y), egui::Align2::LEFT_CENTER, label, font, t.text);
if resp.clicked() {
app.ui.options_pane = id.to_string();
}
@@ -172,7 +165,7 @@ pub fn page(app: &mut WordApp, ui: &mut Ui) {
let (line, _) = ui.allocate_exact_size(vec2(1.0, 420.0), egui::Sense::hover());
ui.painter().rect_filled(line, 0.0, t.border);
ui.add_space(20.0);
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.set_max_width(640.0);
match current.as_str() {
"display" => display(app, ui),
@@ -185,13 +178,7 @@ pub fn page(app: &mut WordApp, ui: &mut Ui) {
_ => general(app, ui),
}
});
};
// Right to left the pane list is on the right and the settings fill in from it.
if rtl {
ui.with_layout(crate::rtl::row_layout(ui, egui::Align::Min), body)
} else {
crate::rtl::horizontal_top(ui, body)
};
});
}
fn section(ui: &mut Ui, title: &str) {
@@ -250,38 +237,27 @@ fn general(app: &mut WordApp, ui: &mut Ui) {
}
ui.add_space(8.0);
section(ui, "Personalize");
crate::rtl::Table::new("options_user").columns(2).spacing(vec2(8.0, 6.0)).show(ui, |t| {
t.row(|r| {
let user_label = r.cell(|ui| ui.label(tl!("User name:")));
r.cell(|ui| {
let mut n = app.session.author.clone();
let protected = wordcraft_engine::cmd::protect::enforced(&app.session.doc);
if ui.add_enabled(!protected, egui::TextEdit::singleline(&mut n).desired_width(260.0)).labelled_by(user_label.id).changed()
&& !protected
{
app.session.author = n;
}
});
});
t.row(|r| {
let initials_label = r.cell(|ui| ui.label(tl!("Initials:")));
r.cell(|ui| {
let mut i = app.session.initials.clone();
let hint = app.session.user_initials();
if ui
.add(
egui::TextEdit::singleline(&mut i)
.desired_width(80.0)
.char_limit(wordcraft_engine::cmd::options::MAX_INITIALS_CHARS)
.hint_text(hint),
)
.labelled_by(initials_label.id)
.changed()
{
set(app, json!({"initials": i}));
}
});
});
egui::Grid::new("options_user").num_columns(2).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
let user_label = ui.label(tl!("User name:"));
let mut n = app.session.author.clone();
let protected = wordcraft_engine::cmd::protect::enforced(&app.session.doc);
if ui.add_enabled(!protected, egui::TextEdit::singleline(&mut n).desired_width(260.0)).labelled_by(user_label.id).changed() && !protected {
app.session.author = n;
}
ui.end_row();
let initials_label = ui.label(tl!("Initials:"));
let mut i = app.session.initials.clone();
let hint = app.session.user_initials();
if ui
.add(
egui::TextEdit::singleline(&mut i).desired_width(80.0).char_limit(wordcraft_engine::cmd::options::MAX_INITIALS_CHARS).hint_text(hint),
)
.labelled_by(initials_label.id)
.changed()
{
set(app, json!({"initials": i}));
}
ui.end_row();
});
note(ui, "New comments and tracked changes are recorded under this name and these initials.");
}
@@ -393,11 +369,10 @@ fn dictionaries(app: &mut WordApp, ui: &mut Ui) {
};
for (language, file, error) in found {
let line = match (file, error) {
(Some(f), Some(e)) => crate::i18n::fmt(
tl!("{language}: {file} (unreadable: {reason})"),
&[("language", &language), ("file", &crate::i18n::isolate_path(&f)), ("reason", &e)],
),
(Some(f), None) => crate::i18n::fmt(tl!("{language}: {file}"), &[("language", &language), ("file", &crate::i18n::isolate_path(&f))]),
(Some(f), Some(e)) => {
crate::i18n::fmt(tl!("{language}: {file} (unreadable: {reason})"), &[("language", &language), ("file", &f), ("reason", &e)])
}
(Some(f), None) => crate::i18n::fmt(tl!("{language}: {file}"), &[("language", &language), ("file", &f)]),
(None, _) => crate::i18n::fmt(tl!("{language}: no dictionary found"), &[("language", &language)]),
};
ui.add(egui::Label::new(RichText::new(line).small()).truncate());
@@ -474,7 +449,7 @@ fn save(app: &mut WordApp, ui: &mut Ui) {
// The browser can't write to the user's files, so AutoSave can't be turned on there (#176).
let browser = app.autosave_block() == Some(crate::AutoSaveBlock::Browser);
let mut autosave = app.autosave && !browser;
let r = ui.add_enabled_ui(!browser, |ui| ui.checkbox(&mut autosave, tl!("AutoSave documents you have saved in WordCraft"))).inner;
let r = ui.add_enabled(!browser, egui::Checkbox::new(&mut autosave, tl!("AutoSave documents you have saved in WordCraft")));
if browser {
r.on_disabled_hover_text(crate::AutoSaveBlock::Browser.reason());
} else if r.changed() {
@@ -562,61 +537,51 @@ fn advanced(app: &mut WordApp, ui: &mut Ui) {
}
ui.add_space(8.0);
section(ui, "Display");
crate::rtl::Table::new("options_display").columns(2).spacing(vec2(8.0, 6.0)).show(ui, |t| {
t.row(|r| {
let recent_label = r.cell(|ui| ui.label(tl!("Recent documents to show:")));
r.cell(|ui| {
let mut n = app.ui.recent_shown();
if ui.add(egui::DragValue::new(&mut n).range(0..=crate::MAX_RECENT).speed(0.2)).labelled_by(recent_label.id).changed() {
app.ui.recent_count = n;
egui::Grid::new("options_display").num_columns(2).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
let recent_label = ui.label(tl!("Recent documents to show:"));
let mut n = app.ui.recent_shown();
if ui.add(egui::DragValue::new(&mut n).range(0..=crate::MAX_RECENT).speed(0.2)).labelled_by(recent_label.id).changed() {
app.ui.recent_count = n;
}
ui.end_row();
let units_label = ui.label(tl!("Measurement units:"));
// The user's choice, else the region's (#838): Automatic follows the region.
let units = app.session.units();
let auto = app.session.prefs.units.is_none();
let name = UNITS.iter().find(|(u, _)| *u == units).map_or("Inches", |(_, n)| n);
let shown = if auto { format!("{} ({})", tl!("Automatic"), tl!(name)) } else { tl!(name).to_string() };
let combo = craft_ui::ComboBox::from_id_salt("options_units").selected_text(shown).width(200.0).show_ui(ui, |ui| {
if ui.selectable_label(auto, tl!("Automatic")).clicked() {
set(app, json!({"units": "auto"}));
}
for (u, label) in UNITS {
if ui.selectable_label(!auto && units == u, tl!(label)).clicked() {
set(app, json!({"units": u}));
}
});
});
t.row(|r| {
let units_label = r.cell(|ui| ui.label(tl!("Measurement units:")));
r.cell(|ui| {
// The user's choice, else the region's (#838): Automatic follows the region.
let units = app.session.units();
let auto = app.session.prefs.units.is_none();
let name = UNITS.iter().find(|(u, _)| *u == units).map_or("Inches", |(_, n)| n);
let shown = if auto { format!("{} ({})", tl!("Automatic"), tl!(name)) } else { tl!(name).to_string() };
let combo = craft_ui::ComboBox::from_id_salt("options_units").selected_text(shown).width(200.0).show_ui(ui, |ui| {
if ui.selectable_label(auto, tl!("Automatic")).clicked() {
set(app, json!({"units": "auto"}));
}
for (u, label) in UNITS {
if ui.selectable_label(!auto && units == u, tl!(label)).clicked() {
set(app, json!({"units": u}));
}
}
});
combo.response.labelled_by(units_label.id);
});
}
});
combo.response.labelled_by(units_label.id);
ui.end_row();
// The region new documents, units and the decimal separator follow; Automatic is the
// system's (#838).
t.row(|r| {
let region_label = r.cell(|ui| ui.label(tl!("Region:")));
r.cell(|ui| {
let chosen = app.session.prefs.region.clone();
let system = &app.session.system_region;
let system_tag =
if system.territory.is_empty() { system.language.clone() } else { format!("{}-{}", system.language, system.territory) };
let automatic = crate::i18n::fmt(tl!("Automatic ({region})"), &[("region", system_tag.as_str())]);
let shown = chosen.clone().unwrap_or_else(|| automatic.clone());
let combo = craft_ui::ComboBox::from_id_salt("options_region").selected_text(shown).width(200.0).show_ui(ui, |ui| {
if ui.selectable_label(chosen.is_none(), automatic.as_str()).clicked() {
set(app, json!({"region": "auto"}));
}
for tag in REGIONS {
if ui.selectable_label(chosen.as_deref() == Some(tag), tag).clicked() {
set(app, json!({"region": tag}));
}
}
});
combo.response.labelled_by(region_label.id);
});
let region_label = ui.label(tl!("Region:"));
let chosen = app.session.prefs.region.clone();
let system = &app.session.system_region;
let system_tag = if system.territory.is_empty() { system.language.clone() } else { format!("{}-{}", system.language, system.territory) };
let automatic = crate::i18n::fmt(tl!("Automatic ({region})"), &[("region", system_tag.as_str())]);
let shown = chosen.clone().unwrap_or_else(|| automatic.clone());
let combo = craft_ui::ComboBox::from_id_salt("options_region").selected_text(shown).width(200.0).show_ui(ui, |ui| {
if ui.selectable_label(chosen.is_none(), automatic.as_str()).clicked() {
set(app, json!({"region": "auto"}));
}
for tag in REGIONS {
if ui.selectable_label(chosen.as_deref() == Some(tag), tag).clicked() {
set(app, json!({"region": tag}));
}
}
});
combo.response.labelled_by(region_label.id);
ui.end_row();
});
let mut keytips = app.ui.keytips_enabled;
if ui.checkbox(&mut keytips, tl!("Show KeyTips (Alt or F10)")).changed() {
@@ -713,10 +678,7 @@ fn interface_size_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::theme::InterfaceSize;
let size = app.ui.interface_size;
let native = ui.ctx().native_pixels_per_point().unwrap_or(1.0);
// The template keeps the percent sign outside its placeholder, so `fmt` isolates the number
// without it and a right-to-left line draws "(%200)": the sign goes inside the isolate.
let auto = crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &InterfaceSize::AUTO.drawn_percent(native).to_string())])
.replace("\u{2069}%", "%\u{2069}");
let auto = crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &InterfaceSize::AUTO.drawn_percent(native).to_string())]);
let selected = size.percent().map_or_else(|| auto.clone(), |pct| format!("{pct}%"));
ui.horizontal(|ui| {
let label = ui.label(tl!("Interface size:"));
@@ -773,7 +735,6 @@ fn language_picker(app: &mut WordApp, ui: &mut Ui) {
use crate::i18n::{AUTO, Lang};
// The language names are in their own scripts: fonts for them load from the next frame.
app.want_system_cjk = true;
app.want_system_arabic = true;
let system = crate::i18n::system_lang();
let auto_label = crate::i18n::fmt(tl!("Automatic ({language})"), &[("language", system.name())]);
let current = if app.ui.language == AUTO { auto_label.clone() } else { Lang::from_pref(&app.ui.language).name().to_string() };
+2 -4
View File
@@ -29,8 +29,6 @@ pub(crate) use comments::comment_draft;
use comments::comments;
pub(crate) use comments::{CommentDrafts, comment_editor, commit_comment, commit_reply, flush_comment_drafts, has_comment_drafts, reply_editor};
use host::Content;
#[cfg(test)]
pub(crate) use host::apply;
pub use host::show;
pub(crate) use host::{dock_command, overlay_open};
pub(crate) use inspector::describe_props;
@@ -155,7 +153,7 @@ impl PaneContent<Pane> for Content<'_> {
fn body(&mut self, ui: &mut Ui, pane: &Pane) {
let app = &mut *self.app;
crate::rtl::root(ui, |ui| match pane {
match pane {
Pane::SmartArtText => crate::dialogs_smart_art::text_pane(app, ui),
Pane::Navigation => nav(app, ui),
Pane::Clipboard => clipboard(app, ui),
@@ -169,7 +167,7 @@ impl PaneContent<Pane> for Content<'_> {
Pane::RestrictEditing => crate::dialogs_review::restrict_pane(app, ui),
Pane::Proofing => crate::proofing_pane::show(app, ui),
Pane::Comments => comments(app, ui),
})
}
}
fn default_state(&self, pane: &Pane) -> PaneState {
-3
View File
@@ -74,8 +74,6 @@ pub(crate) fn apply(app: &mut WordApp, request: PaneRequest<Pane>) {
match request {
PaneRequest::Close(pane) => close(app, pane),
PaneRequest::Place(Pane::Styles, place) => {
// Physical sides: the host is not RTL-flipped, and Dock Left / Dock Right mean the
// window's left and right (same as `ui.paneDock` / `view.stylesPaneDock`).
let dock = match place {
Place::Docked(Edge::Start) => "left",
Place::Docked(Edge::End) => "right",
@@ -140,7 +138,6 @@ pub(crate) fn dock_command(app: &mut WordApp, p: &Value) -> Result<Value, String
if let Some(dock) = dock {
app.ui.pane_places.entry(key.into()).or_default().dock = Some(dock);
}
// Report the physical side the pane occupies (matches what was stored / requested).
let name = match pane_state(app, pane).place {
Place::Docked(Edge::Start) => "left",
Place::Floating => "float",
+1 -1
View File
@@ -84,7 +84,7 @@ fn level_row(ui: &mut Ui, t: &Tokens, caption: &str, value: &str, dim: bool, tip
let text = egui::RichText::new(value).font(regular(12.5)).color(if dim { t.text_disabled } else { t.text });
ui.add(egui::Label::new(text).wrap());
});
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Min), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
let (r, resp) = ui.allocate_exact_size(vec2(22.0, 20.0), egui::Sense::click());
let enabled = !disabled;
if enabled && resp.hovered() {
+3 -3
View File
@@ -31,7 +31,7 @@ pub(super) fn clipboard(app: &mut WordApp, ui: &mut Ui) {
let _ = app.run("edit.pasteClipboardItem", json!({"index": i}));
app.canvas.want_focus = true;
}
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Min), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
if ui.small_button(tl!("Delete")).clicked() {
let _ = app.run("edit.deleteClipboardItem", json!({"index": i}));
app.canvas.want_focus = true;
@@ -87,14 +87,14 @@ pub(super) fn styles(app: &mut WordApp, ui: &mut Ui) {
for (id, name, is_char, builtin) in list {
ui.horizontal(|ui| {
let r = ui.selectable_label(id == cur, &name);
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(egui::RichText::new(if is_char { "a" } else { "" }).weak());
});
if r.clicked() {
let _ = app.run("para.style", json!({"style": id}));
app.canvas.want_focus = true;
}
crate::rtl::context_menu(&r, |ui| {
r.context_menu(|ui| {
if ui.button(tl!("Modify…")).clicked() {
app.dialog = crate::dialogs::Dialog::modify_style(app, &id);
ui.close();
+8 -35
View File
@@ -114,46 +114,19 @@ pub(crate) fn float_rect(v: &Value) -> Option<[f32; 4]> {
/// A pane's place and size: Styles' from `view.stylesPaneDock` and its own preferences, the
/// others' from `panePlaces`.
///
/// `left` / `right` in the docking API and in saved prefs are **physical** window sides (and the
/// Dock Left / Dock Right menus mean the same). Only an unset default place is mirrored in a
/// right-to-left interface, so a pane that has never been docked still opens on the reading-start
/// side.
pub(super) fn pane_state(app: &WordApp, pane: Pane) -> PaneState {
let (mut state, explicit) = stored_state(app, pane);
if !explicit {
state.place = mirror_default(state.place);
}
state
}
/// Flip a default dock edge in a right-to-left interface so Start (reading start in LTR) opens on
/// the physical right. Explicit and saved left/right are never flipped — see [`pane_state`].
pub(super) fn mirror_default(place: Place) -> Place {
match place {
Place::Docked(Edge::Start) if crate::rtl::on() => Place::Docked(Edge::End),
Place::Docked(Edge::End) if crate::rtl::on() => Place::Docked(Edge::Start),
other => other,
}
}
/// Physical place from prefs, and whether that place was explicitly saved (`true`) or is still the
/// pane's default (`false`).
fn stored_state(app: &WordApp, pane: Pane) -> (PaneState, bool) {
if pane == Pane::Styles {
return (styles_state(app), true);
return styles_state(app);
}
let default = pane.default_state();
let Some(saved) = app.ui.pane_places.get(pane.key()) else {
return (default, false);
let Some(saved) = app.ui.pane_places.get(pane.key()) else { return default };
let place = match saved.dock {
Some(SavedDock::Left) => Place::Docked(Edge::Start),
Some(SavedDock::Right) => Place::Docked(Edge::End),
Some(SavedDock::Float) => Place::Floating,
None => default.place,
};
let (place, explicit) = match saved.dock {
Some(SavedDock::Left) => (Place::Docked(Edge::Start), true),
Some(SavedDock::Right) => (Place::Docked(Edge::End), true),
Some(SavedDock::Float) => (Place::Floating, true),
None => (default.place, false),
};
(PaneState { place, size: saved.width.or(default.size), float_rect: saved.float }, explicit)
PaneState { place, size: saved.width.or(default.size), float_rect: saved.float }
}
/// The Styles pane's place and size, from `view.stylesPaneDock` and the UI preferences (#1756).
-31
View File
@@ -378,37 +378,6 @@ fn pane_dock_command_docks_floats_and_reads_each_pane() {
assert_eq!(super::pane_state(&app, Pane::Navigation).place, Place::Floating, "junk changed nothing");
}
/// Explicit `left` / `right` stay on the physical window sides in an Arabic (RTL) interface;
/// defaults still flip to the reading-start side. Covers `ui.paneDock` and `view.stylesPaneDock`.
#[test]
fn pane_dock_left_and_right_are_physical_in_rtl() {
use craft_ui::panes::{Edge, Place};
use serde_json::json;
let arabic = crate::i18n::fixture_lang("ar", "\tHome\tالرئيسية\n");
crate::i18n::set_current(arabic);
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("")), crate::Services::default());
// Defaults: Navigation's Start opens on the physical right (reading start) until set.
assert_eq!(super::pane_state(&app, Pane::Navigation).place, Place::Docked(Edge::End));
assert_eq!(app.run("ui.paneDock", json!({"pane": "navigation"})).unwrap()["dock"], "right");
// Explicit left stays physical left and is reported as left.
assert_eq!(app.run("ui.paneDock", json!({"pane": "navigation", "dock": "left"})).unwrap()["dock"], "left");
assert_eq!(super::pane_state(&app, Pane::Navigation).place, Place::Docked(Edge::Start));
assert_eq!(app.run("ui.paneDock", json!({"pane": "navigation"})).unwrap()["dock"], "left");
assert_eq!(app.run("ui.paneDock", json!({"pane": "comments", "dock": "right"})).unwrap()["dock"], "right");
assert_eq!(super::pane_state(&app, Pane::Comments).place, Place::Docked(Edge::End));
// Styles through view.stylesPaneDock — same physical contract.
assert_eq!(app.run("view.stylesPaneDock", json!({"dock": "left"})).unwrap()["dock"], "left");
assert_eq!(super::pane_state(&app, Pane::Styles).place, Place::Docked(Edge::Start));
assert_eq!(app.run("view.stylesPaneDock", json!({})).unwrap()["dock"], "left");
assert_eq!(app.run("view.stylesPaneDock", json!({"dock": "right"})).unwrap()["dock"], "right");
assert_eq!(super::pane_state(&app, Pane::Styles).place, Place::Docked(Edge::End));
// A host Place request (Dock Left menu / drag) saves physical left without flipping.
super::apply(&mut app, craft_ui::panes::PaneRequest::Place(Pane::Clipboard, Place::Docked(Edge::Start)));
assert_eq!(app.ui.pane_places.get("clipboard").and_then(|p| p.dock), Some(super::SavedDock::Left));
assert_eq!(super::pane_state(&app, Pane::Clipboard).place, Place::Docked(Edge::Start));
crate::i18n::set_current(crate::i18n::Lang::EN);
}
/// #3032: Delete is greyed out for built-in styles in the Styles pane (the engine refuses
/// them — `crates/engine/src/cmd/para.rs`), while custom styles keep it enabled.
#[test]
+3 -73
View File
@@ -399,7 +399,7 @@ pub fn ink_mask(img: &mut egui::ColorImage) {
}
}
/// A font list entry: `name` in its own face, 244×22 pt. The face is read for the preview alone
/// A font list entry: `name` in its own face, 240×20 pt. The face is read for the preview alone
/// and dropped (#848): browsing the list used to load every family shown for good, so each trip
/// through the Font box grew memory by tens of megabytes. A name the font has no glyphs for (a
/// symbol font's), or a font that can't be read, shows in the default face.
@@ -409,36 +409,8 @@ pub fn font_entry_image(name: &str, ppp: f32) -> egui::ColorImage {
.preview_face(name)
.filter(|f| name.chars().filter(|c| !c.is_whitespace()).all(|c| f.covers(c)))
.unwrap_or_else(|| db.face(wordcraft_fonts::FALLBACK_FAMILY, "Regular"));
// Rendered taller than the row and cut back to the drawn text: faces with tall line metrics
// (Arabic, CJK) put their baseline well below a Latin face's, and a fixed 22 pt strip showed
// only the top of their names.
let img = wordcraft_render::render_text_line(&face, name, 13.0, 244.0, FONT_PREVIEW_RENDER_H, 2.0, ppp);
let img = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_straight());
center_rows_on_ink(&img, (FONT_PREVIEW_H * ppp).round() as usize)
}
/// Height of a font-menu entry's picture, in points.
const FONT_PREVIEW_H: f32 = 22.0;
/// Height the entry is first rendered at, tall enough for any face's first line at 13 pt.
const FONT_PREVIEW_RENDER_H: f32 = 56.0;
/// `img` cut to `rows` pixel rows centred on what is drawn in it (the rows that differ from the
/// top-left background pixel). An image that is already that short, or blank, keeps its top rows.
fn center_rows_on_ink(img: &egui::ColorImage, rows: usize) -> egui::ColorImage {
let [w, h] = img.size;
let rows = rows.clamp(1, h.max(1));
if w == 0 || h <= rows {
return img.clone();
}
let background = img.pixels.first().copied().unwrap_or_default();
let inked =
|y: usize| img.pixels.get(y.saturating_mul(w)..y.saturating_add(1).saturating_mul(w)).is_some_and(|row| row.iter().any(|p| *p != background));
let top = (0..h).find(|y| inked(*y)).unwrap_or(0);
let bottom = (0..h).rev().find(|y| inked(*y)).unwrap_or(top);
let middle = top.saturating_add(bottom).saturating_add(1) / 2;
let start = middle.saturating_sub(rows / 2).min(h.saturating_sub(rows));
let pixels = img.pixels.get(start.saturating_mul(w)..start.saturating_add(rows).saturating_mul(w)).unwrap_or_default().to_vec();
egui::ColorImage::new([w, rows], pixels)
let img = wordcraft_render::render_text_line(&face, name, 13.0, 244.0, 22.0, 2.0, ppp);
egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_straight())
}
/// Lay out `para` (with `base`'s styles) in a `w`×`h` pt box and rasterise it.
@@ -712,45 +684,3 @@ pub fn table_style_tile(ui: &mut Ui, app: &mut WordApp, style: &str) -> Response
#[cfg(test)]
mod tests;
#[test]
fn font_preview_rows_are_centred_on_the_drawn_text() {
let ink = egui::Color32::BLACK;
let paper = egui::Color32::WHITE;
// Ink low in a tall picture, as a face with a deep baseline draws it.
let mut img = egui::ColorImage::filled([4, 40], paper);
for y in 28..34 {
img.pixels[y * 4 + 1] = ink;
}
let cut = center_rows_on_ink(&img, 10);
assert_eq!(cut.size, [4, 10]);
let rows: Vec<usize> = (0..10).filter(|y| cut.pixels[y * 4 + 1] == ink).collect();
assert_eq!(rows, [2, 3, 4, 5, 6, 7], "all six inked rows, with equal space above and below");
// Blank, short, empty and oversized requests never panic.
assert_eq!(center_rows_on_ink(&egui::ColorImage::filled([4, 40], paper), 10).size, [4, 10]);
assert_eq!(center_rows_on_ink(&img, 400).size, [4, 40]);
assert_eq!(center_rows_on_ink(&img, 0).size, [4, 1]);
assert_eq!(center_rows_on_ink(&egui::ColorImage::filled([0, 0], paper), 10).size, [0, 0]);
}
/// Every loaded face's name fits its font-menu row: faces with tall line metrics (the
/// craft-fonts Arabic and Japanese faces) were cut off at the bottom.
#[test]
fn font_preview_shows_the_whole_name_in_every_loaded_face() {
let ppp = 2.0;
let rows = (FONT_PREVIEW_H * ppp).round() as usize;
for family in wordcraft_fonts::FontDb::with_font_dirs(Vec::new()).families() {
let face = wordcraft_fonts::FontDb::global().face(&family, "Regular");
let tall = wordcraft_render::render_text_line(&face, &family, 13.0, 244.0, FONT_PREVIEW_RENDER_H, 2.0, ppp);
let tall = egui::ColorImage::from_rgba_unmultiplied([tall.width as usize, tall.height as usize], &tall.to_straight());
let img = center_rows_on_ink(&tall, rows);
let [w, h] = img.size;
assert_eq!(h, rows, "{family}");
let background = img.pixels[0];
let inked = |y: usize| img.pixels[y * w..(y + 1) * w].iter().any(|p| *p != background);
assert!((0..h).any(inked), "{family}: the name is drawn");
assert!(!inked(0) && !inked(h - 1), "{family}: the name touches the edge of its row");
// Nothing of the name is left outside the cut.
let all = (0..tall.size[1]).filter(|y| tall.pixels[y * w..(y + 1) * w].iter().any(|p| *p != tall.pixels[0])).count();
assert_eq!((0..h).filter(|y| inked(*y)).count(), all, "{family}: part of the name is cut off");
}
}
+14 -50
View File
@@ -156,15 +156,10 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
let current = app.session.current_pages(&lay);
app.ui.print.preview = app.ui.print.preview.min(total.saturating_sub(1));
let settings = |app: &mut WordApp, ui: &mut Ui| {
{
ui.set_width(SETTINGS_WIDTH);
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.set_width(380.0);
// The settings scroll on their own: the preview beside them keeps the whole height (#1156).
// egui draws the bar at the right edge, over the right-aligned controls of a mirrored
// column: a solid bar there takes its own gutter instead of floating over them.
if crate::rtl::flows(ui) {
ui.spacing_mut().scroll = egui::style::ScrollStyle::solid();
}
egui::ScrollArea::vertical().id_salt("print_settings").show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 8.0;
let native = app.services.print_native.is_some();
@@ -203,7 +198,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
if native {
if app.ui.print.copies > 1 {
ui.add_enabled_ui(!app.printing.busy(), |ui| ui.checkbox(&mut app.ui.print.collate, tl!("Collate copies")));
ui.add_enabled(!app.printing.busy(), egui::Checkbox::new(&mut app.ui.print.collate, tl!("Collate copies")));
}
} else {
let copies_label = ui.label(egui::RichText::new(tl!("Copies")).font(medium(13.0)));
@@ -260,37 +255,12 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
}
export_formats(app, ui);
});
}
};
// A right-to-left interface splits the room by hand, settings on the side reading starts from:
// a `horizontal_top` row there lays the preview over the settings.
if crate::rtl::flows(ui) {
let room = ui.available_rect_before_wrap();
let split = (room.max.x - SETTINGS_WIDTH).max(room.min.x);
let settings_rect = Rect::from_min_max(pos2(split, room.min.y), room.max);
let preview_rect = Rect::from_min_max(room.min, pos2((split - SETTINGS_GAP).max(room.min.x), room.max.y));
let layout = egui::Layout::top_down(egui::Align::Max);
ui.scope_builder(egui::UiBuilder::new().max_rect(settings_rect).layout(layout), |ui| settings(app, ui));
// A window too narrow for a usable preview shows the settings alone.
if preview_rect.width() >= MIN_PREVIEW_WIDTH {
ui.scope_builder(egui::UiBuilder::new().max_rect(preview_rect).layout(layout), |ui| preview(app, ui, &lay, total));
}
ui.allocate_rect(room, Sense::hover());
return;
}
crate::rtl::horizontal_top(ui, |ui| {
crate::rtl::vertical(ui, |ui| settings(app, ui));
ui.add_space(SETTINGS_GAP);
crate::rtl::vertical(ui, |ui| preview(app, ui, &lay, total));
});
ui.add_space(24.0);
ui.vertical(|ui| preview(app, ui, &lay, total));
});
}
/// The settings column's width, and the space between it and the preview.
const SETTINGS_WIDTH: f32 = 380.0;
const SETTINGS_GAP: f32 = 24.0;
/// The narrowest preview pane worth drawing when the layout is mirrored.
const MIN_PREVIEW_WIDTH: f32 = 200.0;
/// Room around the page inside the preview pane.
const PREVIEW_MARGIN: f32 = 10.0;
/// The preview's bottom bar: the page indicator on the left, the zoom on the right.
@@ -331,7 +301,6 @@ fn preview(app: &mut WordApp, ui: &mut Ui, lay: &DocLayout, total: usize) {
ui.painter().image(tex.id(), on_sheet, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), egui::Color32::WHITE);
}
});
let rtl = crate::rtl::flows(ui);
ui.horizontal(|ui| {
ui.set_height(PREVIEW_BAR);
let nav = |ui: &mut Ui, icon: &str, tip: &str, enabled: bool| {
@@ -344,26 +313,21 @@ fn preview(app: &mut WordApp, ui: &mut Ui, lay: &DocLayout, total: usize) {
icons::paint(ui.painter(), r.shrink(4.0), icon, col, col);
enabled && resp.on_hover_text(tip).clicked()
};
if nav(ui, if rtl { "chevronRight" } else { "chevronLeft" }, tl!("Previous Page"), page > 0) {
if nav(ui, "chevronLeft", tl!("Previous Page"), page > 0) {
app.ui.print.preview = page - 1;
}
ui.label(egui::RichText::new(&label).font(regular(13.0)));
if nav(ui, if rtl { "chevronLeft" } else { "chevronRight" }, tl!("Next Page"), page + 1 < total) {
if nav(ui, "chevronRight", tl!("Next Page"), page + 1 < total) {
app.ui.print.preview = page + 1;
}
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let z = scale / crate::canvas::PX_PER_PT;
if nav(ui, "onePage", tl!("One Page"), true) {
app.ui.print.zoom = None;
}
ui.label(egui::RichText::new(format!("{}%", (z * 100.0).round())).font(regular(13.0)));
// The slider grows to the right in every language, as in the status bar: minus at its
// left end, plus at its right. This cluster fills from the far end inwards.
let (inner, outer) = (("minus", "Zoom Out", false), ("plus", "Zoom In", true));
let (near, far) = if rtl { (inner, outer) } else { (outer, inner) };
let enabled = |up: bool| if up { z < 5.0 } else { z > 0.1 };
if nav(ui, near.0, tl!(near.1), enabled(near.2)) {
app.ui.print.zoom = Some(zoom_step(z, near.2));
if nav(ui, "plus", tl!("Zoom In"), z < 5.0) {
app.ui.print.zoom = Some(zoom_step(z, true));
}
// A narrow window shortens the slider, then drops it, before it covers the page indicator.
let room = ui.available_width() - 26.0 - 2.0 * ui.spacing().item_spacing.x - 12.0;
@@ -376,8 +340,8 @@ fn preview(app: &mut WordApp, ui: &mut Ui, lay: &DocLayout, total: usize) {
app.ui.print.zoom = Some(v.clamp(0.1, 5.0));
}
}
if nav(ui, far.0, tl!(far.1), enabled(far.2)) {
app.ui.print.zoom = Some(zoom_step(z, far.2));
if nav(ui, "minus", tl!("Zoom Out"), z > 0.1) {
app.ui.print.zoom = Some(zoom_step(z, false));
}
});
});
-1
View File
@@ -373,7 +373,6 @@ mod tests {
#[test]
#[ignore = "offscreen visual review; requires a software or hardware graphics adapter"]
fn spelling_review_dialog_preview() {
let _gpu = crate::table_resize::tests::gpu_turn();
let mut a = app("The quick brwn fox.");
a.run("review.spelling", json!({})).unwrap();
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1000.0, 700.0)).wgpu().build_ui_state(frame, a);
+170 -219
View File
@@ -130,200 +130,184 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
.exact_size(30.0)
.frame(egui::Frame::NONE.fill(t.tab_strip).inner_margin(egui::Margin { left: 8, right: 10, top: 0, bottom: 0 }))
.show(ui, |ui| {
crate::rtl::root(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing = vec2(2.0, 0.0);
// Share, Editing mode and Comments keep their room; the tabs get what is left of it
// (#897, #2026), and the buttons give up their labels before a tab is drawn over them.
let share = tl!("Share");
let editing_label = if app.session.doc.settings.track_changes { tl!("Reviewing") } else { tl!("Editing") };
let comments = tl!("Comments");
let share_w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(share.to_string(), medium(12.0), t.text).size().x);
let spaced = |w: f32| w + 2.0;
let button_widths = |level: usize| -> f32 {
let labels = level == 0;
let share_full = (share_w + 36.0).max(74.0);
let share_button = if level < 2 { share_full } else { 30.0 };
let editing = crate::widgets::icon_text_button_width(ui, editing_label, regular(12.0), true, labels);
let comment = crate::widgets::icon_text_button_width(ui, comments, regular(12.0), false, labels);
// The gaps (6 + 4 + 8) and the strip's margin.
spaced(share_button) + spaced(editing) + spaced(comment) + 18.0 + 12.0
};
let button_levels = [button_widths(0), button_widths(1), button_widths(2)];
let labels: Vec<&str> = tabs.iter().map(|(tab, custom)| if *custom { tab.as_str() } else { tl!(tab) }).collect();
let specs: Vec<TabSpec> = tabs
.iter()
.zip(&labels)
.map(|((tab, custom), shown)| TabSpec {
text_w: ui.ctx().fonts_mut(|f| f.layout_no_wrap(shown.to_string(), medium(12.5), t.text).size().x),
file: tab == "File",
active: *tab == app.ui.tab,
contextual: !custom && wordcraft_engine::ribbon::CONTEXTUAL_TABS.contains(&tab.as_str()),
})
.collect();
let plan = plan_strip(&specs, button_levels, ui.available_width(), MORE_TABS_W + 2.0);
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
// No document open (#1129): nothing to share, edit or comment on — but only
// these buttons. Disabling the whole line took the tab strip with it: the
// tabs took clicks that did nothing, so the empty window had no way back to
// the Start screen (#3045).
ui.add_enabled_ui(app.has_document(), |ui| {
let (share_size, share_text) =
if plan.buttons < 2 { (((share_w + 36.0).max(74.0), 24.0), true) } else { ((30.0, 24.0), false) };
let (r, resp) = ui.allocate_exact_size(vec2(share_size.0, share_size.1), Sense::click());
ui.painter().rect_filled(r, 4.0, if resp.hovered() { t.accent_text } else { t.accent });
let rtl = crate::rtl::on();
let icon_x = if share_text { crate::rtl::lead_x(rtl, r, 14.0) } else { r.center().x };
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(icon_x, r.center().y), vec2(14.0, 14.0)),
"share",
t.on_accent,
t.on_accent,
);
if share_text {
ui.painter().text(
pos2(crate::rtl::lead_x(rtl, r, 26.0), r.center().y),
crate::rtl::lead_center(rtl),
share,
medium(12.0),
t.on_accent,
);
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing = vec2(2.0, 0.0);
// Share, Editing mode and Comments keep their room; the tabs get what is left of it
// (#897, #2026), and the buttons give up their labels before a tab is drawn over them.
let share = tl!("Share");
let editing_label = if app.session.doc.settings.track_changes { tl!("Reviewing") } else { tl!("Editing") };
let comments = tl!("Comments");
let share_w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(share.to_string(), medium(12.0), t.text).size().x);
let spaced = |w: f32| w + 2.0;
let button_widths = |level: usize| -> f32 {
let labels = level == 0;
let share_full = (share_w + 36.0).max(74.0);
let share_button = if level < 2 { share_full } else { 30.0 };
let editing = crate::widgets::icon_text_button_width(ui, editing_label, regular(12.0), true, labels);
let comment = crate::widgets::icon_text_button_width(ui, comments, regular(12.0), false, labels);
// The gaps (6 + 4 + 8) and the strip's margin.
spaced(share_button) + spaced(editing) + spaced(comment) + 18.0 + 12.0
};
let button_levels = [button_widths(0), button_widths(1), button_widths(2)];
let labels: Vec<&str> = tabs.iter().map(|(tab, custom)| if *custom { tab.as_str() } else { tl!(tab) }).collect();
let specs: Vec<TabSpec> = tabs
.iter()
.zip(&labels)
.map(|((tab, custom), shown)| TabSpec {
text_w: ui.ctx().fonts_mut(|f| f.layout_no_wrap(shown.to_string(), medium(12.5), t.text).size().x),
file: tab == "File",
active: *tab == app.ui.tab,
contextual: !custom && wordcraft_engine::ribbon::CONTEXTUAL_TABS.contains(&tab.as_str()),
})
.collect();
let plan = plan_strip(&specs, button_levels, ui.available_width(), MORE_TABS_W + 2.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// No document open (#1129): nothing to share, edit or comment on — but only
// these buttons. Disabling the whole line took the tab strip with it: the
// tabs took clicks that did nothing, so the empty window had no way back to
// the Start screen (#3045).
ui.add_enabled_ui(app.has_document(), |ui| {
let (share_size, share_text) =
if plan.buttons < 2 { (((share_w + 36.0).max(74.0), 24.0), true) } else { ((30.0, 24.0), false) };
let (r, resp) = ui.allocate_exact_size(vec2(share_size.0, share_size.1), Sense::click());
ui.painter().rect_filled(r, 4.0, if resp.hovered() { t.accent_text } else { t.accent });
let icon_x = if share_text { r.min.x + 14.0 } else { r.center().x };
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(icon_x, r.center().y), vec2(14.0, 14.0)),
"share",
t.on_accent,
t.on_accent,
);
if share_text {
ui.painter().text(pos2(r.min.x + 26.0, r.center().y), Align2::LEFT_CENTER, share, medium(12.0), t.on_accent);
}
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, share));
if resp.on_hover_text(tl!("Export a copy to share (PDF, Word document)")).clicked() {
let _ = app.run("ui.backstage", json!({"value": true, "page": "export"}));
}
ui.add_space(6.0);
let track = app.session.doc.settings.track_changes;
let mode = if plan.buttons == 0 {
crate::widgets::icon_text_menu_button(ui, "pencil", editing_label, regular(12.0))
} else {
crate::widgets::icon_only_button(ui, "pencil", editing_label, regular(12.0), true)
};
egui::Popup::menu(&mode).show(|ui| {
if ui.selectable_label(!track, tl!(tl!("Editing — edit the document directly"))).clicked() {
let _ = app.run("review.trackChanges", json!({"value": false}));
ui.close();
}
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, share));
if resp.on_hover_text(tl!("Export a copy to share (PDF, Word document)")).clicked() {
let _ = app.run("ui.backstage", json!({"value": true, "page": "export"}));
}
ui.add_space(6.0);
let track = app.session.doc.settings.track_changes;
let mode = if plan.buttons == 0 {
crate::widgets::icon_text_menu_button(ui, "pencil", editing_label, regular(12.0))
} else {
crate::widgets::icon_only_button(ui, "pencil", editing_label, regular(12.0), true)
};
crate::rtl::menu(&mode).show(|ui| {
if ui.selectable_label(!track, tl!(tl!("Editing — edit the document directly"))).clicked() {
let _ = app.run("review.trackChanges", json!({"value": false}));
ui.close();
}
if ui.selectable_label(track, tl!(tl!("Reviewing — edits become suggestions"))).clicked() {
let _ = app.run("review.trackChanges", json!({"value": true}));
ui.close();
}
});
ui.add_space(4.0);
let comments_button = if plan.buttons == 0 {
crate::widgets::icon_text_button(ui, "comment", comments, regular(12.0))
} else {
crate::widgets::icon_only_button(ui, "comment", comments, regular(12.0), false)
};
if comments_button.clicked() {
let _ = app.run("view.commentsPane", json!({}));
if ui.selectable_label(track, tl!(tl!("Reviewing — edits become suggestions"))).clicked() {
let _ = app.run("review.trackChanges", json!({"value": true}));
ui.close();
}
});
ui.add_space(8.0);
// The tabs fill the room left of the cluster, from the side reading starts from.
// (By language: this row sits in the far-side cluster's layout, which fills from the other end.)
let tabs_layout = if crate::rtl::on() {
egui::Layout::right_to_left(egui::Align::Center)
ui.add_space(4.0);
let comments_button = if plan.buttons == 0 {
crate::widgets::icon_text_button(ui, "comment", comments, regular(12.0))
} else {
egui::Layout::left_to_right(egui::Align::Center)
crate::widgets::icon_only_button(ui, "comment", comments, regular(12.0), false)
};
ui.with_layout(tabs_layout, |ui| {
// The tabs that don't fit go into a » menu at the end of the strip (#897, #2026).
for (i, ((tab, custom), _)) in tabs.iter().zip(&plan.fits).enumerate().filter(|(_, (_, fits))| **fits) {
let (tab, custom) = (tab.as_str(), *custom);
let contextual = !custom && wordcraft_engine::ribbon::CONTEXTUAL_TABS.contains(&tab);
// A custom tab's name is the user's own text: shown as typed.
let shown = if custom { tab } else { tl!(tab) };
let w = plan.widths.get(i).copied().unwrap_or(0.0) - 2.0;
let cut = specs.get(i).is_some_and(|sp| sp.text_w + plan.pad > w + 0.5);
let (r, resp) = ui.allocate_exact_size(vec2(w, 30.0), Sense::click());
if app.ui.keytips == crate::keytips::Phase::Tabs {
crate::keytips::record_tab(r, tab, &app.ui.keytip_prefix, &mut app.keytip_rects);
} else if app.ui.keytips == crate::keytips::Phase::Legacy {
// Every tab: they only place the legacy keys' hint (#1697).
crate::keytips::record_tab(r, tab, "", &mut app.keytip_rects);
}
let active = app.ui.tab == tab && !app.ui.backstage;
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, active, shown));
if resp.hovered() && !active {
ui.painter().rect_filled(r.shrink2(vec2(0.0, 4.0)), 4.0, t.hover);
}
let color = if contextual || active { t.accent_text } else { t.text };
if cut {
let mut job = egui::text::LayoutJob::single_section(
shown.to_string(),
egui::text::TextFormat::simple(if active { semibold(12.5) } else { medium(12.5) }, color),
);
job.wrap = egui::text::TextWrapping {
max_width: (w - plan.pad).max(1.0),
max_rows: 1,
break_anywhere: true,
overflow_character: Some('…'),
};
let galley = ui.painter().layout_job(job);
ui.painter().galley(r.center() - galley.size() / 2.0, galley, color);
} else {
ui.painter().text(
r.center(),
Align2::CENTER_CENTER,
shown,
if active { semibold(12.5) } else { medium(12.5) },
color,
);
}
let resp = if cut { resp.on_hover_text(shown) } else { resp };
if active {
let u = Rect::from_center_size(pos2(r.center().x, r.max.y - 2.0), vec2(w - 16.0, 3.0));
ui.painter().rect_filled(u, 2.0, t.accent);
}
if resp.clicked() {
let _ = app.run("ui.tab", json!({"tab": tab}));
}
if resp.double_clicked() && tab != "File" {
let _ = app.run("ui.collapseRibbon", json!({}));
}
resp.context_menu(|ui| crate::widgets::customize_menu_items(ui, app));
if comments_button.clicked() {
let _ = app.run("view.commentsPane", json!({}));
}
});
ui.add_space(8.0);
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
// The tabs that don't fit go into a » menu at the end of the strip (#897, #2026).
for (i, ((tab, custom), _)) in tabs.iter().zip(&plan.fits).enumerate().filter(|(_, (_, fits))| **fits) {
let (tab, custom) = (tab.as_str(), *custom);
let contextual = !custom && wordcraft_engine::ribbon::CONTEXTUAL_TABS.contains(&tab);
// A custom tab's name is the user's own text: shown as typed.
let shown = if custom { tab } else { tl!(tab) };
let w = plan.widths.get(i).copied().unwrap_or(0.0) - 2.0;
let cut = specs.get(i).is_some_and(|sp| sp.text_w + plan.pad > w + 0.5);
let (r, resp) = ui.allocate_exact_size(vec2(w, 30.0), Sense::click());
if app.ui.keytips == crate::keytips::Phase::Tabs {
crate::keytips::record_tab(r, tab, &app.ui.keytip_prefix, &mut app.keytip_rects);
} else if app.ui.keytips == crate::keytips::Phase::Legacy {
// Every tab: they only place the legacy keys' hint (#1697).
crate::keytips::record_tab(r, tab, "", &mut app.keytip_rects);
}
let left_out: Vec<(&str, &str)> = tabs
.iter()
.zip(&labels)
.zip(&plan.fits)
.filter(|(_, fits)| !**fits)
.map(|(((tab, _), shown), _)| (tab.as_str(), *shown))
.collect();
if !left_out.is_empty() {
let (r, resp) = ui.allocate_exact_size(vec2(MORE_TABS_W, 30.0), Sense::click());
if resp.hovered() {
ui.painter().rect_filled(r.shrink2(vec2(0.0, 4.0)), 4.0, t.hover);
}
// The active tab is in the menu: the button says so.
let holds_active = left_out.iter().any(|(tab, _)| *tab == app.ui.tab);
let active = app.ui.tab == tab && !app.ui.backstage;
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, active, shown));
if resp.hovered() && !active {
ui.painter().rect_filled(r.shrink2(vec2(0.0, 4.0)), 4.0, t.hover);
}
let color = if contextual || active { t.accent_text } else { t.text };
if cut {
let mut job = egui::text::LayoutJob::single_section(
shown.to_string(),
egui::text::TextFormat::simple(if active { semibold(12.5) } else { medium(12.5) }, color),
);
job.wrap = egui::text::TextWrapping {
max_width: (w - plan.pad).max(1.0),
max_rows: 1,
break_anywhere: true,
overflow_character: Some('…'),
};
let galley = ui.painter().layout_job(job);
ui.painter().galley(r.center() - galley.size() / 2.0, galley, color);
} else {
ui.painter().text(
r.center(),
Align2::CENTER_CENTER,
"»",
medium(12.5),
if holds_active { t.accent_text } else { t.text },
shown,
if active { semibold(12.5) } else { medium(12.5) },
color,
);
// "»" means nothing read aloud: the button says what it holds.
let more = tl!("More tabs");
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, more));
let resp = resp.on_hover_text(more);
crate::rtl::menu(&resp).show(|ui| {
for (tab, shown) in left_out {
if ui.selectable_label(app.ui.tab == tab, shown).clicked() {
let _ = app.run("ui.tab", json!({"tab": tab}));
ui.close();
}
}
});
}
});
let resp = if cut { resp.on_hover_text(shown) } else { resp };
if active {
let u = Rect::from_center_size(pos2(r.center().x, r.max.y - 2.0), vec2(w - 16.0, 3.0));
ui.painter().rect_filled(u, 2.0, t.accent);
}
if resp.clicked() {
let _ = app.run("ui.tab", json!({"tab": tab}));
}
if resp.double_clicked() && tab != "File" {
let _ = app.run("ui.collapseRibbon", json!({}));
}
resp.context_menu(|ui| crate::widgets::customize_menu_items(ui, app));
}
let left_out: Vec<(&str, &str)> = tabs
.iter()
.zip(&labels)
.zip(&plan.fits)
.filter(|(_, fits)| !**fits)
.map(|(((tab, _), shown), _)| (tab.as_str(), *shown))
.collect();
if !left_out.is_empty() {
let (r, resp) = ui.allocate_exact_size(vec2(MORE_TABS_W, 30.0), Sense::click());
if resp.hovered() {
ui.painter().rect_filled(r.shrink2(vec2(0.0, 4.0)), 4.0, t.hover);
}
// The active tab is in the menu: the button says so.
let holds_active = left_out.iter().any(|(tab, _)| *tab == app.ui.tab);
ui.painter().text(
r.center(),
Align2::CENTER_CENTER,
"»",
medium(12.5),
if holds_active { t.accent_text } else { t.text },
);
// "»" means nothing read aloud: the button says what it holds.
let more = tl!("More tabs");
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, more));
let resp = resp.on_hover_text(more);
egui::Popup::menu(&resp).show(|ui| {
for (tab, shown) in left_out {
if ui.selectable_label(app.ui.tab == tab, shown).clicked() {
let _ = app.run("ui.tab", json!({"tab": tab}));
ui.close();
}
}
});
}
});
});
})
});
});
if app.ui.ribbon_collapsed {
return;
@@ -341,32 +325,19 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
ui.disable();
}
let bg = ui.interact(ui.max_rect(), ui.scope_id().with("ribbon_background"), Sense::click());
crate::rtl::context_menu(&bg, |ui| crate::rtl::root(ui, |ui| crate::widgets::customize_menu_items(ui, app)));
bg.context_menu(|ui| crate::widgets::customize_menu_items(ui, app));
let entry = user_tabs.iter().find(|e| e.name == app.ui.tab);
crate::ribbon_fit::begin(ui.ctx(), &app.ui.tab, ui.available_width() - 4.0);
// 0.37 keeps the scroll offset private: an edge-chevron click leaves a one-frame
// request here and the next scroll area is built at that offset.
let pending_id = ui.scope_id().with("ribbon_scroll_pending");
let pending = ui.data(|d| d.get_temp::<f32>(pending_id));
// Right to left, the groups start at the right edge. A scroll area's content is
// unbounded, so the row gets a width: the view's, or last frame's content if wider.
// The scroll position is kept as a distance from the right, where reading starts.
let rtl = crate::rtl::on();
let view_w = ui.available_width();
let row_id = ui.scope_id().with(("ribbon_row", app.ui.tab.as_str()));
let (content_w, from_right, last_over) = ui.data(|d| d.get_temp::<(f32, f32, f32)>(row_id)).unwrap_or((0.0, 0.0, 0.0));
let row_w = content_w.max(view_w);
let set_offset = (last_over - from_right).max(0.0);
let mut area = egui::ScrollArea::horizontal().scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden);
if rtl {
area = area.horizontal_scroll_offset(set_offset);
} else if let Some(x) = pending {
if let Some(x) = pending {
area = area.horizontal_scroll_offset(x);
}
let mut measured = 0.0;
let scroll = area.show(ui, |ui| {
let size = vec2(if rtl { row_w } else { ui.available_size_before_wrap().x }, ui.available_size_before_wrap().y);
let body = |ui: &mut Ui| {
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing = vec2(2.0, 2.0);
match entry {
// Every tab hidden: nothing to show.
@@ -400,24 +371,8 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
if let Some(e) = entry {
custom_groups(app, ui, &e.groups);
}
};
if rtl {
measured = ui.allocate_ui_with_layout(size, egui::Layout::right_to_left(egui::Align::Min), body).response.rect.width();
} else {
crate::rtl::horizontal_top(ui, body);
}
});
});
if rtl {
let over = (scroll.content_size.x - scroll.inner_rect.width()).max(0.0);
// Only the user's own scrolling moves the remembered distance: while the width
// is still settling, the offset set above lags a frame behind.
let scrolled = (scroll.state.clamped_offset().x - set_offset).abs() > 0.5 && set_offset <= over;
let from_right = if scrolled { (over - scroll.state.clamped_offset().x).max(0.0) } else { from_right.min(over) };
ui.data_mut(|d| d.insert_temp(row_id, (measured, from_right, over)));
if (over - last_over).abs() > 0.5 {
ui.ctx().request_repaint();
}
}
if pending.is_some() {
let _ = ui.data_mut(|d| d.remove_temp::<f32>(pending_id));
}
@@ -442,11 +397,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
}
}
if let Some(x) = next {
let x = x.clamp(0.0, overflow.max(0.0));
if rtl {
ui.data_mut(|d| d.insert_temp(row_id, (measured, (overflow - x).max(0.0), overflow.max(0.0))));
}
ui.data_mut(|d| d.insert_temp(pending_id, x));
ui.data_mut(|d| d.insert_temp(pending_id, x.clamp(0.0, overflow.max(0.0))));
ui.ctx().request_repaint();
}
});
+8 -10
View File
@@ -55,16 +55,14 @@ pub(super) fn wrap_menu(ui: &mut Ui, app: &mut WordApp) {
}
ui.separator();
ui.menu_button(tl!("Wrap Text Sides"), |ui| {
crate::rtl::root(ui, |ui| {
for (l, s, v) in [
("Both Sides", WrapText::BothSides, "bothSides"),
("Left Only", WrapText::Left, "left"),
("Right Only", WrapText::Right, "right"),
("Largest Only", WrapText::Largest, "largest"),
] {
mi_check(ui, app, l, side == s, "arrange.wrapText", json!({"side": v}));
}
})
for (l, s, v) in [
("Both Sides", WrapText::BothSides, "bothSides"),
("Left Only", WrapText::Left, "left"),
("Right Only", WrapText::Right, "right"),
("Largest Only", WrapText::Largest, "largest"),
] {
mi_check(ui, app, l, side == s, "arrange.wrapText", json!({"side": v}));
}
});
});
}
+1 -1
View File
@@ -28,7 +28,7 @@ pub(super) fn smart_art_design(app: &mut WordApp, ui: &mut Ui) {
});
group(ui, "Layouts", None, app, |ui, app| {
let t = Tokens::get(ui.ctx());
crate::rtl::grid(ui, egui::Grid::new("smart_art_layout_gallery").spacing(vec2(2.0, 2.0)), |ui| {
egui::Grid::new("smart_art_layout_gallery").spacing(vec2(2.0, 2.0)).show(ui, |ui| {
for (k, layout) in SmartArtLayout::ALL.into_iter().enumerate() {
let (r, resp) = ui.allocate_exact_size(vec2(46.0, 31.0), Sense::click());
let on = g.layout == layout;
+2 -2
View File
@@ -66,7 +66,7 @@ pub(super) fn esc_cancelled(ui: &Ui, r: &egui::Response, id: egui::Id) -> bool {
}
pub(super) fn stack(ui: &mut Ui, add: impl FnOnce(&mut Ui)) {
crate::rtl::vertical(ui, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(1.0, 0.0);
add(ui);
});
@@ -110,7 +110,7 @@ pub(super) fn mi_icon(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, i
pub(super) fn mi_text(ui: &mut Ui, app: &mut WordApp, text: &str, id: &str, params: Value) {
let sc = crate::widgets::shortcut_text(app, id);
let enabled = crate::widgets::enabled(app, id);
let resp = ui.add_enabled(enabled, crate::rtl::menu_item(ui, text, &sc, 200.0));
let resp = ui.add_enabled(enabled, egui::Button::new(text).shortcut_text(sc).min_size(vec2(200.0, 0.0)));
if resp.clicked() {
crate::widgets::run_from_ribbon(app, id, params);
ui.close();
+1 -1
View File
@@ -116,7 +116,7 @@ fn gallery_pen(ui: &mut Ui, app: &mut WordApp, i: usize, pen: wordcraft_engine::
if resp.clicked() {
let _ = app.run(pen_command(pen.kind), json!({"color": pen.color.hex(), "width": pen.width}));
}
crate::rtl::context_menu(&resp, |ui| {
resp.context_menu(|ui| {
if ui.button(tl!("Remove Pen")).clicked() {
let _ = app.run("draw.removePen", json!({"index": i}));
ui.close();
+4 -6
View File
@@ -68,11 +68,9 @@ pub(super) fn insert(app: &mut WordApp, ui: &mut Ui) {
mi_check(ui, app, "Draw Table", drawing, "table.draw", json!({}));
mi(ui, app, "Convert Text to Table…", "table.fromText", json!({}));
ui.menu_button(tl!("Quick Tables"), |ui| {
crate::rtl::root(ui, |ui| {
mi(ui, app, "Tabular List", "table.quick", json!({"kind": "tabular"}));
mi(ui, app, "Matrix", "table.quick", json!({"kind": "matrix"}));
mi(ui, app, "Calendar", "table.quick", json!({"kind": "calendar"}));
})
mi(ui, app, "Tabular List", "table.quick", json!({"kind": "tabular"}));
mi(ui, app, "Matrix", "table.quick", json!({"kind": "matrix"}));
mi(ui, app, "Calendar", "table.quick", json!({"kind": "calendar"}));
});
});
});
@@ -207,7 +205,7 @@ pub(super) fn insert(app: &mut WordApp, ui: &mut Ui) {
}
}
items.truncate(30);
crate::rtl::grid(ui, egui::Grid::new("syms"), |ui| {
egui::Grid::new("syms").show(ui, |ui| {
for (i, (c, font)) in items.iter().enumerate() {
let b = ui.button(egui::RichText::new(c).size(16.0));
let b = if font.is_empty() { b } else { b.on_hover_text(font) };
+1 -1
View File
@@ -136,7 +136,7 @@ pub(super) fn layout(app: &mut WordApp, ui: &mut Ui) {
ui.label(text);
});
};
crate::rtl::grid(ui, egui::Grid::new("layout_para").spacing(vec2(4.0, 1.5)).min_row_height(FIELD.y), |ui| {
egui::Grid::new("layout_para").spacing(vec2(4.0, 1.5)).min_row_height(FIELD.y).show(ui, |ui| {
ui.label(egui::RichText::new(tl!("Indent")).small().strong());
ui.label("");
spaced(ui, egui::RichText::new(tl!("Spacing")).small().strong());
+1 -1
View File
@@ -21,7 +21,7 @@ pub(crate) fn gradient_menu(ui: &mut Ui, app: &mut WordApp, fill: Option<&wordcr
}
for (title, tiles) in ["Light Variations", "Dark Variations"].into_iter().zip(shape_fill::variations(main)) {
ui.label(egui::RichText::new(tl!(title)).small().weak());
crate::rtl::grid(ui, egui::Grid::new(title).spacing(vec2(4.0, 4.0)), |ui| {
egui::Grid::new(title).spacing(vec2(4.0, 4.0)).show(ui, |ui| {
for (i, (f, name)) in tiles.iter().enumerate() {
if crate::format_shape::swatch(ui, f, name) {
let _ = app.run("shape.fill", shape_fill::params(Some(f)));
+9 -11
View File
@@ -5,7 +5,7 @@ pub(super) fn table_design(app: &mut WordApp, ui: &mut Ui) {
let look = app.session.sel.focus.path.cell().and_then(|(tp, _, _)| app.session.doc.table(app.session.sel.focus.story, &tp).map(|t| t.props.look));
group(ui, "Table Style Options", None, app, |ui, app| {
let Some(l) = look else { return };
crate::rtl::grid(ui, egui::Grid::new("look"), |ui| {
egui::Grid::new("look").show(ui, |ui| {
for (row, items) in [
[("Header Row", "headerRow", l.header_row), ("First Column", "firstColumn", l.first_column)],
[("Total Row", "totalRow", l.total_row), ("Last Column", "lastColumn", l.last_column)],
@@ -37,16 +37,14 @@ pub(super) fn table_design(app: &mut WordApp, ui: &mut Ui) {
.map(|s| (s.builtin, s.id.clone(), crate::i18n::style_name(s)))
.collect();
styles.sort_by_key(|(builtin, _, _)| *builtin);
crate::rtl::island(ui, |ui| {
egui::ScrollArea::horizontal().max_width(420.0).show(ui, |ui| {
ui.horizontal(|ui| {
for (_, id, name) in styles {
if crate::previews::table_style_tile(ui, app, &id).on_hover_text(name).clicked() {
let _ = app.run("table.style", json!({"style": id}));
}
egui::ScrollArea::horizontal().max_width(420.0).show(ui, |ui| {
ui.horizontal(|ui| {
for (_, id, name) in styles {
if crate::previews::table_style_tile(ui, app, &id).on_hover_text(name).clicked() {
let _ = app.run("table.style", json!({"style": id}));
}
});
})
}
});
});
stack(ui, |ui| {
let current = look.and_then(|_| {
@@ -289,7 +287,7 @@ pub(super) fn table_layout(app: &mut WordApp, ui: &mut Ui) {
});
});
group(ui, "Alignment", None, app, |ui, app| {
crate::rtl::grid(ui, egui::Grid::new("cellalign").spacing(vec2(1.0, 1.0)), |ui| {
egui::Grid::new("cellalign").spacing(vec2(1.0, 1.0)).show(ui, |ui| {
for row in [
[("topLeft", "Align Top Left"), ("topCenter", "Align Top Center"), ("topRight", "Align Top Right")],
[("centerLeft", "Align Center Left"), ("center", "Align Center"), ("centerRight", "Align Center Right")],
+67 -72
View File
@@ -21,82 +21,77 @@ pub(super) fn reading_toolbar(app: &mut WordApp, ui: &mut Ui, t: Tokens) {
.exact_size(46.0)
.frame(egui::Frame::NONE.fill(t.ribbon).inner_margin(egui::Margin::symmetric(12, 6)).stroke(Stroke::new(1.0, t.border)))
.show(ui, |ui| {
crate::rtl::root(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.label(egui::RichText::new(tl!("Reading Mode")).font(semibold(14.0)));
ui.separator();
let w = app.session.view.read_column_width;
let active = match w {
None => "Auto",
Some(v) if v <= 180.0 => "Narrow",
Some(v) if v <= 240.0 => "Medium",
Some(v) if v <= 350.0 => "Wide",
_ => "Page",
};
ui.menu_button(format!("{}: {}", tl!("Column width"), tl!(active)), |ui| {
for (name, value) in [("Auto", "auto"), ("Narrow", "narrow"), ("Medium", "medium"), ("Wide", "wide"), ("Page", "page")] {
if ui.selectable_label(active == name, tl!(name)).clicked() {
let _ = app.run("view.readColumnWidth", json!({"value": value}));
ui.close();
}
}
});
ui.separator();
let pct = (app.session.view.read_zoom * 100.0).round() as u32;
ui.menu_button(format!("{}: {pct}%", tl!("Text size")), |ui| {
for percent in [75, 90, 100, 110, 125, 150] {
if ui.selectable_label(pct == percent, format!("{percent}%")).clicked() {
let _ = app.run("view.readTextSize", json!({"value": percent}));
ui.close();
}
}
});
ui.separator();
let spacing = app.session.view.read_line_spacing;
let label = spacing.map_or_else(|| tl!("Document").to_owned(), |v| format!("{v}×"));
ui.menu_button(format!("{}: {}", tl!("Line spacing"), label), |ui| {
if ui.selectable_label(spacing.is_none(), tl!("Document")).clicked() {
let _ = app.run("view.readLineSpacing", json!({"value": "document"}));
ui.horizontal_centered(|ui| {
ui.label(egui::RichText::new(tl!("Reading Mode")).font(semibold(14.0)));
ui.separator();
let w = app.session.view.read_column_width;
let active = match w {
None => "Auto",
Some(v) if v <= 180.0 => "Narrow",
Some(v) if v <= 240.0 => "Medium",
Some(v) if v <= 350.0 => "Wide",
_ => "Page",
};
ui.menu_button(format!("{}: {}", tl!("Column width"), tl!(active)), |ui| {
for (name, value) in [("Auto", "auto"), ("Narrow", "narrow"), ("Medium", "medium"), ("Wide", "wide"), ("Page", "page")] {
if ui.selectable_label(active == name, tl!(name)).clicked() {
let _ = app.run("view.readColumnWidth", json!({"value": value}));
ui.close();
}
for value in [1.0, 1.25, 1.5, 2.0] {
if ui.selectable_label(spacing == Some(value), format!("{value}×")).clicked() {
let _ = app.run("view.readLineSpacing", json!({"value": value}));
ui.close();
}
}
});
ui.separator();
use wordcraft_engine::cmd::view::ReadPageColor;
let colors = [
(ReadPageColor::None, "None", "none"),
(ReadPageColor::Sepia, "Sepia", "sepia"),
(ReadPageColor::Inverse, "Inverse", "inverse"),
];
let current = app.session.view.read_page_color;
let name = colors.iter().find(|c| c.0 == current).map_or("None", |c| c.1);
ui.menu_button(format!("{}: {}", tl!("Page color"), tl!(name)), |ui| {
for (color, name, value) in colors {
if ui.selectable_label(current == color, tl!(name)).clicked() {
let _ = app.run("view.readPageColor", json!({"value": value}));
ui.close();
}
}
});
ui.separator();
// Headings to jump to, in the Navigation pane beside the columns (Outline view
// is an editing layout, so it isn't offered here).
if ui.button(tl!("Navigation Pane")).clicked() {
let _ = app.run("view.navigationPane", json!({"value": true}));
app.ui.nav_tab = "headings".into();
}
ui.with_layout(crate::rtl::far_side(ui, egui::Align::Center), |ui| {
if ui.button(tl!("Edit Document")).clicked() {
let _ = app.run("view.printLayout", json!({}));
}
});
});
})
ui.separator();
let pct = (app.session.view.read_zoom * 100.0).round() as u32;
ui.menu_button(format!("{}: {pct}%", tl!("Text size")), |ui| {
for percent in [75, 90, 100, 110, 125, 150] {
if ui.selectable_label(pct == percent, format!("{percent}%")).clicked() {
let _ = app.run("view.readTextSize", json!({"value": percent}));
ui.close();
}
}
});
ui.separator();
let spacing = app.session.view.read_line_spacing;
let label = spacing.map_or_else(|| tl!("Document").to_owned(), |v| format!("{v}×"));
ui.menu_button(format!("{}: {}", tl!("Line spacing"), label), |ui| {
if ui.selectable_label(spacing.is_none(), tl!("Document")).clicked() {
let _ = app.run("view.readLineSpacing", json!({"value": "document"}));
ui.close();
}
for value in [1.0, 1.25, 1.5, 2.0] {
if ui.selectable_label(spacing == Some(value), format!("{value}×")).clicked() {
let _ = app.run("view.readLineSpacing", json!({"value": value}));
ui.close();
}
}
});
ui.separator();
use wordcraft_engine::cmd::view::ReadPageColor;
let colors =
[(ReadPageColor::None, "None", "none"), (ReadPageColor::Sepia, "Sepia", "sepia"), (ReadPageColor::Inverse, "Inverse", "inverse")];
let current = app.session.view.read_page_color;
let name = colors.iter().find(|c| c.0 == current).map_or("None", |c| c.1);
ui.menu_button(format!("{}: {}", tl!("Page color"), tl!(name)), |ui| {
for (color, name, value) in colors {
if ui.selectable_label(current == color, tl!(name)).clicked() {
let _ = app.run("view.readPageColor", json!({"value": value}));
ui.close();
}
}
});
ui.separator();
// Headings to jump to, in the Navigation pane beside the columns (Outline view
// is an editing layout, so it isn't offered here).
if ui.button(tl!("Navigation Pane")).clicked() {
let _ = app.run("view.navigationPane", json!({"value": true}));
app.ui.nav_tab = "headings".into();
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Edit Document")).clicked() {
let _ = app.run("view.printLayout", json!({}));
}
});
});
});
}
-350
View File
@@ -1,350 +0,0 @@
//! Mirrored window layout for a right-to-left interface language (Arabic).
//!
//! egui has no global direction, but `ui.horizontal` runs right to left wherever the enclosing
//! layout prefers it (`Layout::prefer_right_to_left`): a right-to-left row, or a column whose
//! items are aligned to the right. So direction is inherited from a root: each panel, window and
//! menu body is wrapped in [`root`], columns are opened with [`vertical`] (egui's `ui.vertical`
//! always resets to left-aligned), and "far side" clusters use [`far_side`]. Widgets that paint
//! their own parts ask [`flows`] where their leading edge is.
//!
//! An area that egui lays out left to right on its own (a `Grid`, a far-side cluster) stays left
//! to right inside, because everything here follows the enclosing layout rather than the language.
//! The document itself is never mirrored: its direction belongs to the document.
use egui::{Align, Align2, InnerResponse, Layout, Rect, Ui, UiBuilder, pos2};
use craft_ui::direction::{self, Direction};
fn dir(rtl: bool) -> Direction {
if rtl { Direction::Rtl } else { Direction::Ltr }
}
/// Install the interface direction on `ctx` when the language's has changed: the shared
/// `craft_ui::direction` state, and egui's own chrome (check boxes and radio buttons with the mark
/// at the reading start, submenus, scroll bars, title bars, the font combo) through
/// `Style::right_to_left`.
pub fn sync(ctx: &egui::Context) {
let want = dir(on());
if direction::HasDirection::direction(ctx) != want {
direction::apply(ctx, want);
}
}
/// Is the interface language written right to left?
pub fn on() -> bool {
crate::i18n::current().rtl()
}
/// Is `ui` part of the mirrored interface: a right-to-left language, in an area that lays its
/// content out from the right (so rows opened in it run right to left)? Never in a left-to-right
/// language, whose far-side clusters also fill from the right but are not mirrored.
pub fn flows(ui: &Ui) -> bool {
on() && ui.layout().prefer_right_to_left()
}
/// Run a container's body (a panel, window, popup or menu, which egui starts left-aligned) in the
/// interface's direction. In a left-to-right language this is just `add`.
pub fn root<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> R {
if !on() || flows(ui) {
return add(ui);
}
ui.scope_builder(UiBuilder::new().layout(Layout::top_down(Align::Max)), add).inner
}
/// `ui.vertical`, aligned to the side rows start from: a column inside a right-to-left area keeps
/// its content on the right and its rows right to left.
pub fn vertical<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
if flows(ui) { ui.scope_builder(UiBuilder::new().layout(Layout::top_down(Align::Max)), add) } else { ui.vertical(add) }
}
/// `ui.horizontal_top`, with its items running from the side reading starts from: a row opened
/// in a right-to-left area is a right-to-left layout of its own, so its first column sits at the
/// right edge.
pub fn horizontal_top<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
if flows(ui) { ui.scope_builder(UiBuilder::new().layout(Layout::right_to_left(Align::Min)), add) } else { ui.horizontal_top(add) }
}
/// The layout for a row's items that belong at its far end (the side rows end on): filled from
/// that end inwards.
pub fn far_side(ui: &Ui, align: Align) -> Layout {
if flows(ui) { Layout::left_to_right(align) } else { Layout::right_to_left(align) }
}
/// A row layout in the direction of `ui`, for `allocate_ui_with_layout` and `with_layout`.
pub fn row_layout(ui: &Ui, align: Align) -> Layout {
if flows(ui) { Layout::right_to_left(align) } else { Layout::left_to_right(align) }
}
/// The x coordinate `inset` points in from the leading edge of `rect` (left, or right when `rtl`).
pub fn lead_x(rtl: bool, rect: Rect, inset: f32) -> f32 {
let d = dir(rtl);
direction::start_x(&d, rect) + direction::forward(&d) * inset
}
/// The x coordinate `inset` points in from the trailing edge of `rect`.
pub fn trail_x(rtl: bool, rect: Rect, inset: f32) -> f32 {
lead_x(!rtl, rect, inset)
}
/// Text anchored at its leading edge and vertically centred: left-centre, or right-centre when `rtl`.
pub fn lead_center(rtl: bool) -> Align2 {
direction::align2(&dir(rtl), Align2::LEFT_CENTER)
}
/// Text anchored at its leading edge and top.
pub fn lead_top(rtl: bool) -> Align2 {
direction::align2(&dir(rtl), Align2::LEFT_TOP)
}
/// Where to put something `width` wide so that it starts `inset` in from the leading edge of
/// `rect`: its left x.
pub fn lead_left(rtl: bool, rect: Rect, inset: f32, width: f32) -> f32 {
if rtl { rect.max.x - inset - width } else { rect.min.x + inset }
}
/// The leading top corner of `rect`.
pub fn lead_corner(rtl: bool, rect: Rect) -> egui::Pos2 {
pos2(lead_x(rtl, rect, 0.0), rect.min.y)
}
/// A side panel on the side reading starts from (left; right in a right-to-left interface).
pub fn start_panel(id: &'static str) -> egui::Panel {
direction::start_panel(&dir(on()), id)
}
/// A side panel on the side reading ends on (right; left in a right-to-left interface).
pub fn end_panel(id: &'static str) -> egui::Panel {
direction::end_panel(&dir(on()), id)
}
/// `egui::Popup::menu`, with its items aligned to the side reading starts from.
pub fn menu(response: &egui::Response) -> egui::Popup<'_> {
let popup = egui::Popup::menu(response);
if on() { popup.layout(menu_layout()) } else { popup }
}
/// A right-to-left menu's layout: items against the right edge. Not justified, because egui
/// starts a full-width button's text at its left.
fn menu_layout() -> Layout {
Layout::top_down(Align::Max)
}
/// A menu item: `text`, with its keyboard shortcut at the far side. `min_width` keeps a menu's
/// items one width.
pub fn menu_item<'a>(ui: &Ui, text: &'a str, shortcut: &'a str, min_width: f32) -> egui::Button<'a> {
let button = if flows(ui) {
egui::Button::new(egui::RichText::new(shortcut).weak()).right_text(text)
} else {
egui::Button::new(text).shortcut_text(shortcut)
};
button.min_size(egui::vec2(min_width, 0.0))
}
/// `Response::context_menu`, with its items aligned to the side reading starts from.
pub fn context_menu(response: &egui::Response, add: impl FnOnce(&mut Ui)) {
let popup = egui::Popup::context_menu(response);
let popup = if on() { popup.layout(menu_layout()) } else { popup };
popup.show(add);
}
/// Run `add` in a left-to-right island inside a right-to-left area, for content egui can only
/// lay out from the left (a `Grid`, a horizontally scrolling strip). The island is set against the
/// leading (right) edge using the width it had last frame. Outside a right-to-left area this is
/// just `add`.
pub fn island<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> R {
if !flows(ui) {
return add(ui);
}
// The island gets all the room there is, so its content takes its natural width; it starts
// where content of last frame's width would end at the leading edge. A change of width shows
// one frame late, so that frame asks for another.
let key = ui.next_auto_id().with("rtl_island_width");
let known = ui.data(|d| d.get_temp::<f32>(key));
let room = ui.available_rect_before_wrap();
let available = if room.width().is_finite() { room.width().max(0.0) } else { 0.0 };
let width = known.unwrap_or(available);
let left = room.max.x - width;
if !left.is_finite() {
return add(ui);
}
let max_rect = Rect::from_min_max(pos2(left, room.min.y), pos2(left + available.max(width), room.max.y));
let shown = ui.scope_builder(UiBuilder::new().max_rect(max_rect).layout(Layout::top_down(Align::Min)), add);
let now = shown.response.rect.width();
if now.is_finite() && known.is_none_or(|w| (w - now).abs() > 0.5) {
ui.data_mut(|d| d.insert_temp(key, now));
ui.ctx().request_repaint();
}
shown.inner
}
/// `grid.show(ui, add)`. egui's `Grid` cannot be laid out from the right (it places cells from a
/// left edge that a right-to-left parent does not have, and says so with a debug assertion), so
/// inside a right-to-left area it is drawn in an [`island`]. Its columns keep their order.
pub fn grid<R>(ui: &mut Ui, grid: egui::Grid, add: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
island(ui, |ui| grid.show(ui, add))
}
/// A form of rows and cells. Where the interface is mirrored (`flows`), a row's cells run from the
/// right edge, the first cell at the right, and every column is as wide as its widest cell, so the
/// labels sit at the reading start with their fields after them, to their left. Elsewhere it is
/// exactly an `egui::Grid` with the same id, columns and spacing. A `Grid` itself cannot be laid
/// out from the right, which is why [`grid`] keeps its columns in order.
pub struct Table {
id: &'static str,
columns: usize,
spacing: Option<egui::Vec2>,
}
impl Table {
pub fn new(id: &'static str) -> Self {
Self { id, columns: 2, spacing: None }
}
pub fn columns(mut self, columns: usize) -> Self {
self.columns = columns;
self
}
pub fn spacing(mut self, spacing: egui::Vec2) -> Self {
self.spacing = Some(spacing);
self
}
pub fn show<R>(self, ui: &mut Ui, add: impl FnOnce(&mut Rows) -> R) -> R {
if !flows(ui) {
let spec = egui::Grid::new(self.id).num_columns(self.columns);
let spec = match self.spacing {
Some(spacing) => spec.spacing(spacing),
None => spec,
};
return grid(ui, spec, |ui| add(&mut Rows { ui, mirror: None })).inner;
}
let key = ui.scope_id().with(("rtl_table_widths", self.id));
let prev = ui.data(|d| d.get_temp::<Vec<f32>>(key)).unwrap_or_default();
let spacing = self.spacing.unwrap_or(ui.spacing().item_spacing);
let gap = spacing.x;
let shown = ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = spacing.y;
let mut rows = Rows { ui, mirror: Some(Mirror { prev: prev.clone(), cur: Vec::new(), gap }) };
let out = add(&mut rows);
(out, rows.mirror.map(|m| m.cur).unwrap_or_default())
});
let (out, cur) = shown.inner;
let same = prev.len() == cur.len() && prev.iter().zip(&cur).all(|(a, b)| (a - b).abs() <= 0.5);
if !same {
ui.data_mut(|d| d.insert_temp(key, cur));
ui.ctx().request_repaint();
}
out
}
}
struct Mirror {
/// The width each column had last frame.
prev: Vec<f32>,
/// The width each column has now (the widest of its cells).
cur: Vec<f32>,
gap: f32,
}
/// The rows of a [`Table`].
pub struct Rows<'a> {
ui: &'a mut Ui,
mirror: Option<Mirror>,
}
impl Rows<'_> {
/// One row: add its cells in reading order with [`Cells::cell`].
pub fn row<R>(&mut self, add: impl FnOnce(&mut Cells) -> R) -> R {
match &mut self.mirror {
None => {
let out = add(&mut Cells { ui: self.ui, mirror: None, col: 0, pending: 0.0 });
self.ui.end_row();
out
}
Some(mirror) => {
self.ui
.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
add(&mut Cells { ui, mirror: Some(mirror), col: 0, pending: 0.0 })
})
.inner
}
}
}
}
/// The cells of one row of a [`Table`].
pub struct Cells<'a> {
ui: &'a mut Ui,
mirror: Option<&'a mut Mirror>,
col: usize,
/// The space still to leave before the next cell: what its column is wider than the last cell.
pending: f32,
}
impl Cells<'_> {
/// The next cell of the row (its widgets, in the order they read).
pub fn cell<R>(&mut self, add: impl FnOnce(&mut Ui) -> R) -> R {
let Some(mirror) = self.mirror.as_deref_mut() else {
return add(self.ui);
};
if self.pending > 0.0 {
self.ui.add_space(self.pending);
}
let shown = self.ui.scope(add);
let width = shown.response.rect.width();
let width = if width.is_finite() { width.max(0.0) } else { 0.0 };
if mirror.cur.len() <= self.col {
mirror.cur.resize(self.col + 1, 0.0);
}
if let Some(w) = mirror.cur.get_mut(self.col) {
*w = w.max(width);
}
let column = mirror.prev.get(self.col).copied().unwrap_or(0.0);
self.pending = (column - width).max(0.0) + mirror.gap;
self.col += 1;
shown.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::i18n::{Lang, fixture_lang, set_current};
/// Where a two-cell row puts its label and its field, as the centres of their rectangles.
fn row_centres() -> (f32, f32) {
let ctx = egui::Context::default();
let mut centres = (0.0, 0.0);
// The second frame has the column widths the first measured.
for _ in 0..2 {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
root(ui, |ui| {
Table::new("t").columns(2).show(ui, |t| {
t.row(|r| {
let label = r.cell(|ui| ui.label("Label:"));
let field = r.cell(|ui| ui.add(egui::TextEdit::singleline(&mut String::new()).desired_width(80.0)));
centres = (label.rect.center().x, field.rect.center().x);
});
});
});
});
out.textures_delta.clear();
}
centres
}
/// A form reads from the reading start: the label of a row first, its field after it, so in a
/// right-to-left interface the label is at the right and the field to its left.
#[test]
fn a_form_row_puts_the_label_at_the_reading_start() {
set_current(Lang::EN);
let (label, field) = row_centres();
assert!(label < field, "left to right: label {label}, field {field}");
set_current(fixture_lang("ar", "\tHome\tالرئيسية\n"));
let (label, field) = row_centres();
set_current(Lang::EN);
assert!(label > field, "right to left: label {label}, field {field}");
}
}
-8
View File
@@ -65,14 +65,6 @@ pub(crate) fn steps(h: &mut H, n: usize) {
}
}
/// Held by a test while its harness renders through wgpu: two devices drawing at once crash
/// Windows' software rasterizer (WARP, `d3d10warp.dll`), which is what a machine without a GPU
/// or a remote session gets, so those tests take turns.
pub(crate) fn gpu_turn() -> std::sync::MutexGuard<'static, ()> {
static GPU: std::sync::Mutex<()> = std::sync::Mutex::new(());
GPU.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// A real mouse drag of a column border: the resize arrow on hover, a preview and no change
/// while dragging, then one undo step on release, the selection untouched.
#[test]
+1 -65
View File
@@ -485,22 +485,7 @@ pub fn install_fonts_language_extra(
system_cjk: bool,
faces: &[Arc<wordcraft_fonts::FontFace>],
) {
install_fonts_language_arabic(ctx, prefer_hans, prefer_korean, prefer_hant, system_cjk, false, faces);
}
/// [`install_fonts_language_extra`], also adding an installed Arabic font when `system_arabic` is
/// set and no embedded face has Arabic letters. Asked for by an Arabic interface only, apart from
/// the installed CJK font: reading either is a font file read the other languages never need.
pub fn install_fonts_language_arabic(
ctx: &egui::Context,
prefer_hans: bool,
prefer_korean: bool,
prefer_hant: bool,
system_cjk: bool,
system_arabic: bool,
faces: &[Arc<wordcraft_fonts::FontFace>],
) {
let mut fonts = font_definitions_language_arabic(prefer_hans, prefer_korean, prefer_hant, system_cjk, system_arabic);
let mut fonts = font_definitions_language(prefer_hans, prefer_korean, prefer_hant, system_cjk);
add_fallback_faces(&mut fonts, faces);
ctx.set_fonts(fonts);
}
@@ -556,18 +541,6 @@ pub fn font_definitions(prefer_hans: bool, system_cjk: bool) -> FontDefinitions
/// The interface fonts, ordered for the preferred CJK writing system.
pub fn font_definitions_language(prefer_hans: bool, prefer_korean: bool, prefer_hant: bool, system_cjk: bool) -> FontDefinitions {
font_definitions_language_arabic(prefer_hans, prefer_korean, prefer_hant, system_cjk, false)
}
/// [`font_definitions_language`], also with an installed Arabic font when `system_arabic` is set
/// and the build embeds none.
pub fn font_definitions_language_arabic(
prefer_hans: bool,
prefer_korean: bool,
prefer_hant: bool,
system_cjk: bool,
system_arabic: bool,
) -> FontDefinitions {
let mut fonts = FontDefinitions::default();
let add = |fonts: &mut FontDefinitions, name: &str, bytes: &'static [u8]| {
fonts.font_data.insert(name.into(), Arc::new(FontData::from_static(bytes)));
@@ -625,33 +598,10 @@ pub fn font_definitions_language_arabic(
}
#[cfg(target_arch = "wasm32")]
let _ = system_cjk;
// Arabic-script faces last: the Arabic interface, its name in the language list, Arabic file
// names. Without an embedded one (a build without craft-fonts), an installed font, read when
// `system_arabic` is set: for an Arabic interface.
let arabic = wordcraft_fonts::ui_arabic_fonts();
for f in &arabic {
add_cjk(&mut fonts, format!("{} {}", f.family, f.style), FontData::from_static(f.bytes));
}
#[cfg(not(target_arch = "wasm32"))]
if system_arabic
&& arabic.is_empty()
&& let Some(f) = system_arabic_font()
{
let mut data = FontData::from_static(&f.bytes);
data.index = f.index;
add_cjk(&mut fonts, format!("system {}", f.family), data);
}
// Symbols and emoji fall back to egui's defaults (kept in the families).
fonts
}
/// The installed Arabic interface font, read once.
#[cfg(not(target_arch = "wasm32"))]
fn system_arabic_font() -> Option<&'static wordcraft_fonts::SystemUiFont> {
static ARABIC: std::sync::OnceLock<Option<wordcraft_fonts::SystemUiFont>> = std::sync::OnceLock::new();
ARABIC.get_or_init(wordcraft_fonts::system_arabic_ui_font).as_ref()
}
/// The installed CJK interface font for a Chinese (`hans`) or other interface, chosen once:
/// `None` when the embedded faces already cover the interface text
/// ([`cjk_font_text`](crate::i18n::Lang::cjk_font_text)). The font is picked for the catalogs'
@@ -997,20 +947,6 @@ mod tests {
/// explicitly (kittest's own dev-dependencies do not propagate). WordCraft inserts Inter at
/// the front and SourceSans after egui's complete default fallback block, including the
/// monochrome emoji faces. Production keeps only `default_fonts` through eframe.
/// An installed Arabic font is read for an Arabic interface only (`system_arabic`), and the
/// installed CJK font request alone never brings it in: neither read is paid by the other.
#[test]
fn the_installed_arabic_font_is_asked_for_by_arabic_alone() {
let plain = super::font_definitions_language_arabic(false, false, false, false, false).font_data.len();
let cjk_only = super::font_definitions_language_arabic(false, false, false, true, false);
let arabic = super::font_definitions_language_arabic(false, false, false, false, true).font_data.len();
let installed_arabic = wordcraft_fonts::ui_arabic_fonts().is_empty() && wordcraft_fonts::system_arabic_ui_font().is_some();
assert_eq!(arabic, plain + usize::from(installed_arabic), "Arabic adds the installed font when nothing is embedded");
let arabic_named = |defs: &egui::FontDefinitions| defs.font_data.keys().filter(|k| k.starts_with("system ")).count();
let cjk_font = super::system_cjk_font(false, false).is_some();
assert_eq!(arabic_named(&cjk_only), usize::from(cjk_font), "the CJK request adds the CJK font and no Arabic one");
}
#[test]
fn interface_font_order_keeps_test_default_fonts_explicit() {
let defs = super::font_definitions(false, false);
+3 -5
View File
@@ -122,11 +122,9 @@ pub(crate) fn show(app: &mut WordApp, ctx: &egui::Context) {
.collapsible(false)
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
.show(ctx, |ui| {
crate::rtl::root(ui, |ui| {
if let Some(msg) = &app.updates_rt.status_message {
ui.label(msg);
}
})
if let Some(msg) = &app.updates_rt.status_message {
ui.label(msg);
}
});
if !open {
app.updates_rt.manual_window_open = false;
+8 -13
View File
@@ -1,6 +1,6 @@
use super::groups::command_menu;
use super::tooltip::{keytip_badge, shortcut_text, show_hint, tooltip, tooltip_lines};
use super::{Color32, CornerRadius, Rect, Response, Sense, Stroke, Tokens, Ui, Value, WordApp, icons, pos2, regular, vec2};
use super::{Align2, Color32, CornerRadius, Rect, Response, Sense, Stroke, Tokens, Ui, Value, WordApp, icons, pos2, regular, vec2};
/// Height of the ribbon content area (without group labels).
pub const CONTENT_H: f32 = 66.0;
@@ -147,13 +147,11 @@ pub fn small(ui: &mut Ui, app: &mut WordApp, icon: &str, label: Option<&str>, ti
keytip_badge(ui, r, id, tip);
let on = enabled(app, id);
bg(ui, r, &resp, checked, &t);
let rtl = crate::rtl::flows(ui);
let ic = Rect::from_center_size(pos2(crate::rtl::lead_x(rtl, r, 12.0), r.center().y), vec2(17.0, 17.0));
let ic = Rect::from_center_size(pos2(r.min.x + 12.0, r.center().y), vec2(17.0, 17.0));
let (c, a) = if on { (t.icon, t.accent) } else { (t.text_disabled, t.text_disabled) };
icons::paint(ui.painter(), ic, icon, c, a);
if let Some(l) = label {
let at = pos2(crate::rtl::lead_x(rtl, r, 24.0), r.center().y);
ui.painter().text(at, crate::rtl::lead_center(rtl), l, regular(11.5), if on { t.text } else { t.text_disabled });
ui.painter().text(pos2(r.min.x + 24.0, r.center().y), Align2::LEFT_CENTER, l, regular(11.5), if on { t.text } else { t.text_disabled });
}
let resp = tooltip(app, resp, tip, id);
command_menu(&resp, app, id);
@@ -207,7 +205,7 @@ pub fn split(
if !on {
return;
}
crate::rtl::menu(&aresp).show(|ui| {
egui::Popup::menu(&aresp).show(|ui| {
ui.set_min_width(180.0);
menu(ui, app);
});
@@ -265,14 +263,11 @@ pub fn menu_button(ui: &mut Ui, app: &mut WordApp, options: MenuButtonOptions<'_
let (r, resp) = ui.allocate_exact_size(vec2(24.0 + text_w + 10.0, 22.0), Sense::click());
keytip_badge(ui, r, "menu", tip);
bg(ui, r, &resp, false, &t);
let rtl = crate::rtl::flows(ui);
let lead = |inset: f32| crate::rtl::lead_x(rtl, r, inset);
icons::paint(ui.painter(), Rect::from_center_size(pos2(lead(12.0), r.center().y), vec2(17.0, 17.0)), icon, c, a);
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 12.0, r.center().y), vec2(17.0, 17.0)), icon, c, a);
if let Some(l) = label {
ui.painter().text(pos2(lead(24.0), r.center().y), crate::rtl::lead_center(rtl), l, regular(11.5), text);
ui.painter().text(pos2(r.min.x + 24.0, r.center().y), Align2::LEFT_CENTER, l, regular(11.5), text);
}
let arrow = pos2(crate::rtl::trail_x(rtl, r, 6.0), r.center().y);
icons::paint(ui.painter(), Rect::from_center_size(arrow, vec2(DROPDOWN, DROPDOWN)), "dropdown", c, c);
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.max.x - 6.0, r.center().y), vec2(DROPDOWN, DROPDOWN)), "dropdown", c, c);
resp
};
let reason = if on { None } else { Some(app.session.why_disabled(id).unwrap_or("")) };
@@ -282,7 +277,7 @@ pub fn menu_button(ui: &mut Ui, app: &mut WordApp, options: MenuButtonOptions<'_
if !on {
return;
}
crate::rtl::menu(&resp).show(|ui| {
egui::Popup::menu(&resp).show(|ui| {
ui.set_min_width(200.0);
menu(ui, app);
});
+5 -40
View File
@@ -157,17 +157,6 @@ struct ComboOptions<'a> {
tip: &'a str,
}
/// The arrow's box next to a combo's field: its corner radii and the rectangle it paints, reaching
/// one border width under the field on the side the two meet (the field is left of the arrow in a
/// left-to-right row, right of it in a right-to-left one).
fn arrow_box_shape(rtl: bool, arrow: Rect, r: CornerRadius) -> (CornerRadius, Rect) {
if rtl {
(CornerRadius { nw: r.nw, sw: r.sw, ne: 0, se: 0 }, Rect::from_min_max(arrow.min, pos2(arrow.max.x + ARROW_BORDER, arrow.max.y)))
} else {
(CornerRadius { nw: 0, sw: 0, ne: r.ne, se: r.se }, Rect::from_min_max(pos2(arrow.min.x - ARROW_BORDER, arrow.min.y), arrow.max))
}
}
/// Indices of the `items` matching `query`, ignoring case: exact matches, then the names starting
/// with it, then those containing it, each group in list order. A blank query matches everything.
pub fn font_matches(query: &str, items: &[String]) -> Vec<usize> {
@@ -231,16 +220,12 @@ fn combo_impl(ui: &mut Ui, options: ComboOptions<'_>) -> Option<String> {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
// The field and its arrow share one outline: rounded on the outside, square where they
// meet. The field's frame takes the widget visuals' radius in every state. In a row that
// runs right to left the arrow comes after the field on its left, so the corners and the
// arrow's underlap are the mirror image.
let rtl = ui.layout().prefer_right_to_left();
// meet. The field's frame takes the widget visuals' radius in every state.
let r = ui.visuals().widgets.inactive.corner_radius;
let w = &mut ui.visuals_mut().widgets;
for v in [&mut w.noninteractive, &mut w.inactive, &mut w.hovered, &mut w.active, &mut w.open] {
let (a, b) = if rtl { (&mut v.corner_radius.nw, &mut v.corner_radius.sw) } else { (&mut v.corner_radius.ne, &mut v.corner_radius.se) };
*a = 0;
*b = 0;
v.corner_radius.ne = 0;
v.corner_radius.se = 0;
}
// Up/Down move the list's highlight: take them before the text field moves its cursor.
let mut step = 0i32;
@@ -321,7 +306,8 @@ fn combo_impl(ui: &mut Ui, options: ComboOptions<'_>) -> Option<String> {
}
// It reaches one border width under the field, so the field's right border is the only line
// between them: two borders side by side read as a double-width divider.
let (ar_radius, under) = arrow_box_shape(rtl, ar, r);
let ar_radius = CornerRadius { nw: 0, sw: 0, ne: r.ne, se: r.se };
let under = Rect::from_min_max(pos2(ar.min.x - ARROW_BORDER, ar.min.y), ar.max);
let fill = if aresp.hovered() { t.hover } else { t.input };
let arrow_box = egui::epaint::RectShape::new(under, ar_radius, fill, Stroke::new(ARROW_BORDER, t.input_border), egui::StrokeKind::Inside);
ui.painter().set(arrow_bg, arrow_box);
@@ -616,24 +602,3 @@ pub fn drawn_state(ui: &Ui, resp: &Response, framed: bool) -> DrawnState {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The arrow's box fuses with its field on the side they meet: in a left-to-right row the
/// arrow is right of the field (rounded on the right, painted one border under the field's
/// right edge), in a right-to-left row it is left of it (rounded on the left, under the
/// field's left edge).
#[test]
fn the_arrow_box_is_rounded_away_from_its_field() {
let arrow = Rect::from_min_max(pos2(100.0, 0.0), pos2(116.0, 20.0));
let r = CornerRadius::same(4);
let (ltr, under) = arrow_box_shape(false, arrow, r);
assert_eq!((ltr.nw, ltr.sw, ltr.ne, ltr.se), (0, 0, 4, 4));
assert_eq!((under.min.x, under.max.x), (100.0 - ARROW_BORDER, 116.0));
let (rtl, under) = arrow_box_shape(true, arrow, r);
assert_eq!((rtl.nw, rtl.sw, rtl.ne, rtl.se), (4, 4, 0, 0));
assert_eq!((under.min.x, under.max.x), (100.0, 116.0 + ARROW_BORDER));
}
}
+11 -41
View File
@@ -12,11 +12,7 @@ pub fn group(ui: &mut Ui, title: &str, launcher: Option<&str>, app: &mut WordApp
/// A ribbon group whose title is shown as is (a custom group's name, Customize Ribbon).
pub fn group_titled(ui: &mut Ui, id_title: &str, title: &str, launcher: Option<&str>, app: &mut WordApp, add: impl FnOnce(&mut Ui, &mut WordApp)) {
let t = Tokens::get(ui.ctx());
// Groups follow the ribbon's direction: in a right-to-left interface the content starts at
// the right, and the launcher and the divider sit on the left.
let rtl = crate::rtl::flows(ui);
let start_cursor = ui.cursor();
let start = start_cursor.min;
let start = ui.cursor().min;
// #662: the window may be too narrow for the tab; the plan says how this group fits.
let position = crate::ribbon_fit::position(ui.ctx());
let level = crate::ribbon_fit::level(ui.ctx(), id_title);
@@ -26,10 +22,10 @@ pub fn group_titled(ui: &mut Ui, id_title: &str, title: &str, launcher: Option<&
// Not folded: a drop-down left open while it was folded doesn't come back by itself.
ui.data_mut(|d| d.remove::<bool>(collapsed_open_id(ui, id_title, position)));
let label_w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(title.to_string(), regular(11.0), t.group_label).size().x);
let inner = crate::rtl::vertical(ui, |ui| {
let inner = ui.vertical(|ui| {
ui.set_min_width(label_w + if launcher.is_some() { 22.0 } else { 10.0 });
crate::ribbon_fit::set_compact(ui.ctx(), level == crate::ribbon_fit::COMPACT);
let content = ui.allocate_ui_with_layout(vec2(ui.available_width(), CONTENT_H), crate::rtl::row_layout(ui, egui::Align::Min), |ui| {
let content = ui.allocate_ui_with_layout(vec2(ui.available_width(), CONTENT_H), egui::Layout::left_to_right(egui::Align::Min), |ui| {
ui.spacing_mut().item_spacing = vec2(1.0, 1.0);
add(ui, app);
});
@@ -44,7 +40,7 @@ pub fn group_titled(ui: &mut Ui, id_title: &str, title: &str, launcher: Option<&
let r = inner.response.rect;
title_row(
ui,
Rect::from_min_max(pos2(if rtl { r.min.x } else { start.x }, start.y + CONTENT_H), pos2(r.max.x, start.y + CONTENT_H + LABEL_H)),
Rect::from_min_max(pos2(start.x, start.y + CONTENT_H), pos2(r.max.x, start.y + CONTENT_H + LABEL_H)),
id_title,
title,
launcher,
@@ -52,27 +48,19 @@ pub fn group_titled(ui: &mut Ui, id_title: &str, title: &str, launcher: Option<&
);
}
ui.add_space(5.0);
let x = if rtl { ui.cursor().max.x } else { ui.cursor().min.x };
let x = ui.cursor().min.x;
ui.painter().line_segment([pos2(x, start.y + 4.0), pos2(x, start.y + CONTENT_H + LABEL_H - 4.0)], Stroke::new(1.0, t.border));
ui.add_space(6.0);
crate::ribbon_fit::record(ui.ctx(), id_title, level, group_width(rtl, start_cursor, ui.cursor()));
}
/// The width a group took: the cursor's way along the row, from `before` to `after`. A
/// right-to-left row fills from its right edge, where the cursor's left edge is negative infinity,
/// so there the width is how far the right edge moved.
fn group_width(rtl: bool, before: Rect, after: Rect) -> f32 {
if rtl { before.max.x - after.max.x } else { after.min.x - before.min.x }
crate::ribbon_fit::record(ui.ctx(), id_title, level, ui.cursor().min.x - start.x);
}
/// A group's title row (`r`): the centred title and, with a `launcher`, its dialog launcher.
fn title_row(ui: &mut Ui, r: Rect, id_title: &str, title: &str, launcher: Option<&str>, app: &mut WordApp) {
let t = Tokens::get(ui.ctx());
let title_y = r.center().y;
let rtl = crate::rtl::flows(ui);
ui.painter().text(pos2(r.center().x, title_y), Align2::CENTER_CENTER, title, regular(11.0), t.group_label);
if let Some(cmd) = launcher {
let lr = Rect::from_center_size(pos2(crate::rtl::trail_x(rtl, r, 6.0), title_y), vec2(11.0, 11.0));
let lr = Rect::from_center_size(pos2(r.max.x - 6.0, title_y), vec2(11.0, 11.0));
let resp = ui.interact(lr, ui.scope_id().with(("launch", id_title)), Sense::click());
if resp.hovered() {
ui.painter().rect_filled(lr.expand(2.0), 2.0, t.hover);
@@ -139,9 +127,9 @@ fn collapsed_group(
// This popup isn't counted by `Popup::is_any_open`, so keep the same Esc from also leaving
// full screen.
app.escape_free = false;
crate::rtl::menu(&resp).open_bool(&mut shown).close_behavior(egui::PopupCloseBehavior::IgnoreClicks).show(|ui| {
crate::rtl::vertical(ui, |ui| {
crate::rtl::horizontal_top(ui, |ui| {
egui::Popup::menu(&resp).open_bool(&mut shown).close_behavior(egui::PopupCloseBehavior::IgnoreClicks).show(|ui| {
ui.vertical(|ui| {
ui.horizontal_top(|ui| {
ui.set_min_height(CONTENT_H);
ui.spacing_mut().item_spacing = vec2(1.0, 1.0);
add(ui, app);
@@ -205,7 +193,7 @@ pub fn command_icon(id: &str) -> &str {
/// Right-click on a command's button: put it on (or take it off) the Quick Access Toolbar, or
/// open Customize Ribbon (#379).
pub fn command_menu(resp: &Response, app: &mut WordApp, id: &str) {
crate::rtl::context_menu(resp, |ui| {
resp.context_menu(|ui| {
let known = app.session.registry.get(id).is_some();
let on_qat = app.session.ribbon.qat().contains(&id);
if known && !on_qat && ui.button(tl!("Add to Quick Access Toolbar")).clicked() {
@@ -247,21 +235,3 @@ pub fn row(ui: &mut Ui, add: impl FnOnce(&mut Ui)) {
add(ui);
});
}
#[cfg(test)]
mod tests {
use super::*;
/// A right-to-left row's cursor reaches to negative infinity on the left, so the width of a
/// group in it comes from the right edge: finite and positive, as the ribbon planner needs.
#[test]
fn a_group_in_a_right_to_left_row_has_a_finite_width() {
let before = Rect::from_min_max(pos2(f32::NEG_INFINITY, 0.0), pos2(900.0, 90.0));
let after = Rect::from_min_max(pos2(f32::NEG_INFINITY, 0.0), pos2(740.0, 90.0));
assert_eq!(group_width(true, before, after), 160.0);
assert!(!(before.min.x - after.min.x).is_finite(), "the left edge cannot measure it");
let before = Rect::from_min_max(pos2(20.0, 0.0), pos2(f32::INFINITY, 90.0));
let after = Rect::from_min_max(pos2(180.0, 0.0), pos2(f32::INFINITY, 90.0));
assert_eq!(group_width(false, before, after), 160.0);
}
}
+6 -12
View File
@@ -63,22 +63,19 @@ fn icon_text_button_with(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId
let st = drawn_state(ui, &resp, true);
let r = r.expand(st.expansion);
ui.painter().rect(r, ui.visuals().widgets.inactive.corner_radius, st.fill, st.stroke, egui::StrokeKind::Inside);
// By language, not by layout: these buttons also sit in far-side clusters, which fill from
// the other end.
let rtl = crate::rtl::on();
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(crate::rtl::lead_x(rtl, r, pad.x + icon_w / 2.0), r.center().y), vec2(icon_w, icon_w)),
Rect::from_center_size(pos2(r.min.x + pad.x + icon_w / 2.0, r.center().y), vec2(icon_w, icon_w)),
icon,
t.icon,
t.accent,
);
let text_x = crate::rtl::lead_left(rtl, r, pad.x + icon_w + if show_text { 4.0 } else { 0.0 }, text_w);
let text_x = r.min.x + pad.x + icon_w + if show_text { 4.0 } else { 0.0 };
if show_text {
ui.painter().galley(pos2(text_x, r.center().y - galley.size().y / 2.0), galley, st.text);
}
if marker {
let c = pos2(if rtl { text_x - 4.0 - DROPDOWN / 2.0 } else { text_x + text_w + 4.0 + DROPDOWN / 2.0 }, r.center().y);
let c = pos2(text_x + text_w + 4.0 + DROPDOWN / 2.0, r.center().y);
icons::paint(ui.painter(), Rect::from_center_size(c, vec2(DROPDOWN, DROPDOWN)), "dropdown", st.text, st.text);
}
resp
@@ -121,16 +118,13 @@ pub fn dialog_tabs(ui: &mut Ui, labels: &[String], selected: usize) -> Option<us
let base_y = origin.y + H;
let base = ui.painter().add(egui::Shape::Noop);
let top = CornerRadius { nw: 5, ne: 5, sw: 0, se: 0 };
// Right to left, the tabs start at the right edge and run leftwards.
let rtl = crate::rtl::flows(ui);
let mut x = if rtl { origin.x + width } else { origin.x };
let mut x = origin.x;
let mut clicked = None;
for (i, label) in labels.iter().enumerate() {
let on = i == selected;
let galley = ui.painter().layout_no_wrap(label.clone(), regular(12.5), t.text);
let w = galley.size().x + 28.0;
let left = if rtl { x - w } else { x };
let r = Rect::from_min_max(pos2(left, origin.y + 2.0), pos2(left + w, base_y));
let r = Rect::from_min_max(pos2(x, origin.y + 2.0), pos2(x + w, base_y));
let resp = ui.interact(r, ui.scope_id().with(("dialog_tab", i)), Sense::click());
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, on, label));
let hover = ui.ctx().animate_bool_responsive(resp.id.with("hover"), resp.hovered());
@@ -146,7 +140,7 @@ pub fn dialog_tabs(ui: &mut Ui, labels: &[String], selected: usize) -> Option<us
if resp.clicked() && !on {
clicked = Some(i);
}
x += if rtl { 1.0 - w } else { w - 1.0 };
x += w - 1.0;
}
ui.allocate_space(vec2(width, H + 1.0));
ui.painter().set(base, egui::Shape::line_segment([pos2(origin.x, base_y), pos2(origin.x + width, base_y)], Stroke::new(1.0, t.border)));
-2
View File
@@ -181,9 +181,7 @@ impl WordApp {
fonts_korean: _,
fonts_hant: _,
fonts_system_cjk: _,
fonts_system_arabic: _,
want_system_cjk: _,
want_system_arabic: _,
recent_fonts_for: _,
// File › Recover lists the copies of every window's documents.
recover_fonts_for: _,
+2 -2
View File
@@ -76,7 +76,7 @@ fn paint_style(p: &egui::Painter, r: Rect, id: &str, theme: &[Rgb]) {
/// Our WordArt styles as a grid; a click runs `cmd` with `{"style": id}`.
pub fn style_gallery(ui: &mut Ui, app: &mut WordApp, cmd: &str) {
let theme = app.session.doc.settings.theme_colors.clone();
crate::rtl::grid(ui, egui::Grid::new(("wordart-styles", cmd)).spacing(vec2(4.0, 4.0)), |ui| {
egui::Grid::new(("wordart-styles", cmd)).spacing(vec2(4.0, 4.0)).show(ui, |ui| {
for (i, (id, label)) in ART_STYLES.iter().enumerate() {
if cell(ui, 40.0, 36.0, label, |p, r| paint_style(p, r, id, &theme)) {
crate::widgets::run_from_ribbon(app, cmd, json!({"style": id}));
@@ -196,7 +196,7 @@ pub fn transform_menu(ui: &mut Ui, app: &mut WordApp) {
let t = Tokens::get(ui.ctx());
mi(ui, app, "No Transform", "wordArt.transform", json!({"preset": "none"}));
ui.add_space(4.0);
crate::rtl::grid(ui, egui::Grid::new("wordart-warps").spacing(vec2(4.0, 4.0)), |ui| {
egui::Grid::new("wordart-warps").spacing(vec2(4.0, 4.0)).show(ui, |ui| {
for (i, (id, label)) in DRAWN_WARPS.iter().enumerate() {
if cell(ui, 46.0, 32.0, label, |p, r| paint_warp(p, r.shrink(2.0), id, t.accent)) {
crate::widgets::run_from_ribbon(app, "wordArt.transform", json!({"preset": id}));
-8
View File
@@ -73,14 +73,6 @@ and set `WORDCRAFT_CONTROL_KEY` to that app's key.
Any other method name is treated as a command id: `{"method": "format.bold"}` works.
**Task pane docking.** `ui.paneDock` and `view.stylesPaneDock` use **physical** `left` / `right`
(the window's left and right edges), matching Dock Left / Dock Right in the pane menu. That stays
true in a right-to-left interface language: `{"dock":"left"}` always puts the pane on the physical
left and the command reports `"left"`. A pane that has never been docked keeps its default
start-/end-edge role when mirrored: start-default panes (e.g. Navigation) open on the physical
right in Arabic, end-default panes (e.g. Comments) on the physical left. Saved docks and explicit
requests are never flipped.
Language packs (see [MCP](mcp.md#language-packs)) are commands like any other: `proof.packs`,
`proof.installPack {"locale": "nl"}` and `proof.removePack {"locale": "nl"}`. Through the control
channel they act on the running app's packs, and install only while its downloading setting is on;
+3 -4
View File
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More