1use alloc::collections::BTreeMap;
46use alloc::rc::Rc;
47use alloc::string::String;
48use alloc::vec::Vec;
49
50use text::segment;
51use text::{FS_SYN_BOLD, FS_SYN_ITALIC};
52
53use crate::cmd::{Draw, Gfx};
54use crate::consts::*;
55use crate::ed_core::*;
56use crate::i18n::tr;
57use crate::keys::*;
58use crate::rich_doc::*;
59use crate::rich_html::clip_html;
60use crate::vicon::paint_svg_icon2;
61use crate::{FontSel, Sp, Ui};
62
63pub const RT_READONLY: u32 = 1;
65pub const RT_NO_FRAME: u32 = 2;
67pub const RT_OPEN_LINKS: u32 = 4;
69pub const RT_TAB: u32 = 8;
71pub const RT_PLAIN_PASTE: u32 = 16;
73
74pub const RTB_HISTORY: u32 = 1;
76pub const RTB_STYLE: u32 = 2;
78pub const RTB_CHARS: u32 = 4;
80pub const RTB_COLOR: u32 = 8;
82pub const RTB_LINK: u32 = 16;
84pub const RTB_ALIGN: u32 = 32;
86pub const RTB_LISTS: u32 = 64;
88pub const RTB_CLEAR: u32 = 128;
90pub const RTB_ALL: u32 = 255;
92
93const NAT_W: i32 = 480;
94const NAT_H: i32 = 240;
95const LEVEL_W: i32 = 24; const LIST_W: i32 = 26; const QUOTE_W: i32 = 16; const TAB_W: i32 = 40; #[derive(Clone)]
102struct Piece {
103 a: usize,
104 b: usize,
105 x: i32,
106 w: i32,
107 font: u32,
108 size: i32,
109 asc: i32,
110 flags: u8,
111 color: u32,
112 bg: u32,
113 link: bool,
114 tab: bool,
115}
116
117struct Row {
119 a: usize,
120 b: usize,
121 y: i32,
123 h: i32,
124 base: i32,
125 stops: Vec<(usize, i32)>,
127 pieces: Vec<Piece>,
128}
129
130impl Row {
131 fn x(&self, byte: usize) -> i32 {
132 match self.stops.binary_search_by(|s| s.0.cmp(&byte)) {
133 Ok(i) => self.stops[i].1,
134 Err(0) => self.stops.first().map_or(0, |s| s.1),
135 Err(i) => self.stops[i - 1].1,
136 }
137 }
138 fn hit(&self, x: i32) -> usize {
139 self.stops.iter().min_by_key(|s| (s.1 - x).abs()).map_or(self.a, |s| s.0)
140 }
141 fn char_at(&self, x: i32) -> Option<usize> {
143 let w = self.stops.windows(2).find(|w| x >= w[0].1 && x < w[1].1)?;
144 Some(w[0].0)
145 }
146}
147
148struct PLay {
150 top: i32,
152 h: i32,
153 rows: Vec<Row>,
154 indent: i32,
156 mfont: (u32, i32, i32),
158}
159
160#[derive(Clone, PartialEq, Default)]
161struct LKey {
162 w: i32,
163 dpi: i32,
164 scale: u32,
165 font: String,
166 size: u16,
167 ui: u32,
168}
169
170#[derive(Default)]
172struct View {
173 seen: u32,
174 scroll: i32,
175 key: LKey,
176 lays: BTreeMap<(u32, u8, u8, u8, u8), Rc<PLay>>,
177 t0: u32,
178 prefx: i32,
179 drag: u8,
181 word: (TextPos, TextPos),
182 held: Option<(usize, usize)>,
183 last_sel: TextSel,
184 last_rev: u64,
185 label: (u32, u64, String),
187 link_open: bool,
189 link_text: String,
190 caret_rect: [i32; 4],
191}
192
193#[derive(Default)]
195pub(crate) struct S {
196 views: BTreeMap<u32, View>,
197 link: Option<(u32, String)>,
198 icons: BTreeMap<(usize, u32, u32), Rc<Art>>,
199}
200
201struct Art {
203 art: &'static str,
204 color: u32,
205 accent: u32,
206}
207
208impl Draw for Art {
209 fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
210 let s = r[2].min(r[3]);
211 paint_svg_icon2(g, self.art, r[0] + (r[2] - s) / 2, r[1] + (r[3] - s) / 2, s << 8, self.color, self.accent, 1.75);
212 }
213}
214
215const ART_BOLD: &str = "S2.4M7.5 5H13A3.5 3.5 0 0 1 13 12H7.5ZM7.5 12H14A3.5 3.5 0 0 1 14 19H7.5Z";
217const ART_ITALIC: &str = "M10 5H18M6 19H14M14 5L10 19";
218const ART_UNDER: &str = "M7 4V11A5 5 0 0 0 17 11V4M6 20H18";
219const ART_STRIKE: &str = "M16.5 7.5C15.8 5.9 14.2 5 12 5C9.3 5 7.5 6.3 7.5 8.4C7.5 10 8.6 11 10.4 11.5M4.5 12.5H19.5M8 16.2C8.7 18 10.3 19 12.3 19C15 19 16.8 17.6 16.8 15.6C16.8 14.8 16.6 14.2 16.1 13.6";
220const ART_LEFT: &str = "M4 6H20M4 10H14M4 14H20M4 18H14";
221const ART_CENTER: &str = "M4 6H20M7 10H17M4 14H20M7 18H17";
222const ART_RIGHT: &str = "M4 6H20M10 10H20M4 14H20M10 18H20";
223const ART_JUSTIFY: &str = "M4 6H20M4 10H20M4 14H20M4 18H20";
224const ART_BULLETS: &str = "M9.5 6.5H20M9.5 12H20M9.5 17.5H20|FM6.4 6.5A1.2 1.2 0 1 1 4 6.5A1.2 1.2 0 1 1 6.4 6.5Z|FM6.4 12A1.2 1.2 0 1 1 4 12A1.2 1.2 0 1 1 6.4 12Z|FM6.4 17.5A1.2 1.2 0 1 1 4 17.5A1.2 1.2 0 1 1 6.4 17.5Z";
225const ART_NUMBERS: &str = "M10 6.5H20M10 12H20M10 17.5H20|S1.3M4.4 5.2L5.6 4.4V8.6M4.4 8.6H6.8|S1.3M4.3 11C4.7 10.2 6.7 10.2 6.7 11.4C6.7 12.4 4.3 12.9 4.3 14.2H6.9|S1.3M4.3 16.1C4.8 15.5 6.7 15.5 6.7 16.7C6.7 17.4 6 17.7 5.4 17.7C6.1 17.7 6.9 18 6.9 18.9C6.9 20.1 4.9 20.3 4.2 19.5";
226const ART_INDENT: &str = "M11 6H20M11 10H20M11 14H20M11 18H20|M4 9L7.5 12L4 15";
227const ART_OUTDENT: &str = "M11 6H20M11 10H20M11 14H20M11 18H20|M7.5 9L4 12L7.5 15";
228const ART_LINK: &str = "M10 14L14 10M11 7L13 5A3.5 3.5 0 0 1 19 11L17 13M13 17L11 19A3.5 3.5 0 0 1 5 13L7 11";
229const ART_COLOR: &str = "M7 16L12 4.5L17 16M8.8 12H15.2|AFM4 18.5H20V21.5H4Z";
230const ART_HILITE: &str = "M14.5 4.5L19.5 9.5L11 18H6V13Z|M12.7 6.3L17.7 11.3|AFM3.5 19.5H20.5V22H3.5Z";
231const ART_CLEAR: &str = "M6 5H16M11 5L8.5 17|M14 14.5L19 19.5M19 14.5L14 19.5";
232const ART_UNDO: &str = "M9 14L4 9L9 4|M4 9H14.5A5.5 5.5 0 0 1 14.5 20H8";
233const ART_REDO: &str = "M15 14L20 9L15 4|M20 9H9.5A5.5 5.5 0 0 0 9.5 20H16";
234
235const PALETTE: [u32; 20] = [
237 0xFF000000, 0xFF595959, 0xFF8C8C8C, 0xFFC0C0C0, 0xFFFFFFFF, 0xFFC00000, 0xFFE8590C, 0xFFF2C200, 0xFF2E9E44, 0xFF0F7B8C, 0xFF1F5FBF,
238 0xFF6A3FB5, 0xFFB5338A, 0xFF7F3F00, 0xFF3D6B00, 0xFFFFF27A, 0xFFB7F0A6, 0xFFA6E0F5, 0xFFF5C2E0, 0xFFFFD0A6,
239];
240const SIZES: [u16; 14] = [80, 90, 100, 110, 120, 140, 160, 180, 200, 240, 280, 360, 480, 720];
241
242fn style_scale(s: u8) -> (u32, bool) {
244 match s {
245 PS_H1 => (180, true),
246 PS_H2 => (145, true),
247 PS_H3 => (120, true),
248 _ => (100, false),
249 }
250}
251
252fn opaque(c: u32) -> u32 {
254 if c >> 24 == 0 { c | 0xFF00_0000 } else { c }
255}
256
257struct RView<'a> {
259 doc: &'a RichDoc,
260 lays: &'a [Rc<PLay>],
261 tops: &'a [i32],
262}
263
264impl EdView for RView<'_> {
265 fn lines(&self) -> usize {
266 self.doc.para_count()
267 }
268 fn text(&self, line: usize) -> &str {
269 self.doc.para(line).text()
270 }
271 fn rows(&self, line: usize) -> usize {
272 self.lays[line].rows.len().max(1)
273 }
274 fn row_range(&self, line: usize, row: usize) -> (usize, usize) {
275 let r = &self.lays[line].rows[row];
276 (r.a, r.b)
277 }
278 fn x_at(&self, line: usize, row: usize, byte: usize) -> i32 {
279 self.lays[line].rows[row].x(byte)
280 }
281 fn hit_x(&self, line: usize, row: usize, x: i32) -> usize {
282 self.lays[line].rows[row].hit(x)
283 }
284 fn row_y(&self, line: usize, row: usize) -> (i32, i32) {
285 let l = &self.lays[line];
286 let r = &l.rows[row];
287 (self.tops[line] + l.top + r.y, r.h)
288 }
289 fn row_at(&self, y: i32) -> (usize, usize) {
290 let n = self.lays.len();
291 let mut line = self.tops.partition_point(|&t| t <= y).saturating_sub(1).min(n - 1);
292 let l = &self.lays[line];
293 let ly = y - self.tops[line] - l.top;
294 if ly >= l.rows.last().map_or(0, |r| r.y + r.h) && line + 1 < n && y >= self.tops[line] + l.h {
295 line += 1;
296 }
297 let l = &self.lays[line];
298 let ly = y - self.tops[line] - l.top;
299 let row = l.rows.iter().rposition(|r| r.y <= ly).unwrap_or(0);
300 (line, row)
301 }
302}
303
304impl RView<'_> {
305 fn hit(&self, x: i32, y: i32) -> TextPos {
307 let (line, row) = self.row_at(y);
308 TextPos::new(line, self.hit_x(line, row, x))
309 }
310 fn char_at(&self, x: i32, y: i32) -> Option<TextPos> {
312 let (line, row) = self.row_at(y);
313 let (ry, rh) = self.row_y(line, row);
314 if y < ry || y >= ry + rh {
315 return None;
316 }
317 self.lays[line].rows[row].char_at(x).map(|b| TextPos::new(line, b))
318 }
319}
320
321impl Ui {
322 pub(crate) fn rt_face(&mut self, family: &str, bold: bool, italic: bool) -> u32 {
325 let syn = |ui: &mut Ui, f: u32, flags: u32| -> u32 {
326 if flags == 0 {
327 return f;
328 }
329 let r = ui.text.style(f, flags);
330 if r >= 0 { r as u32 } else { f }
331 };
332 let want = (if bold { FS_SYN_BOLD } else { 0 }) | (if italic { FS_SYN_ITALIC } else { 0 });
333 if !family.is_empty() {
334 if self.font_is_registered(family) {
335 let w = if bold { 700 } else { 400 };
336 if let Some(f) = self.font_registered_face(family, w, italic) {
337 return f;
338 }
339 if let Some(f) = self.font_registered_face(family, w, false) {
340 return syn(self, f, want & FS_SYN_ITALIC);
341 }
342 if let Some(f) = self.font_registered_face(family, 400, italic) {
343 return syn(self, f, want & FS_SYN_BOLD);
344 }
345 if let Some(f) = self.font_registered_face(family, 400, false) {
346 return syn(self, f, want);
347 }
348 } else if family.eq_ignore_ascii_case("monospace") {
349 let (f, _) = self.mono_font(11.0);
350 return syn(self, f, want);
351 } else if let Some(f) = self.font_load(&FontSel { family: String::from(family), weight: if bold { 700 } else { 400 }, italic, size: 11.0, axes: Vec::new() }) {
352 return f;
353 }
354 }
355 let base = if bold { text::FONT_BOLD } else { self.style.ts_font()[TS_BODY as usize] as u32 };
356 syn(self, base, want & FS_SYN_ITALIC)
357 }
358
359 fn rt_px(&self, pt10: u32) -> i32 {
361 let px = pt10 as f32 / 10.0 * 4.0 / 3.0 * self.wp.rt.dpi as f32 / 96.0 * self.text_scale as f32 / 100.0;
362 ((px * 256.0) as i32).clamp(256, 512 * 256)
363 }
364
365 fn rt_lay(&mut self, p: &Para, w: i32, doc_font: &str, base10: u16, faces: &mut Vec<((String, bool, bool), u32)>) -> PLay {
367 let (scale, sbold) = style_scale(p.style);
368 let mut face = |ui: &mut Ui, f: &Fmt| -> (u32, i32) {
369 let fam = if f.font.is_empty() { doc_font } else { f.font.as_str() };
370 let bold = f.flags & RF_BOLD != 0 || sbold;
371 let italic = f.flags & RF_ITALIC != 0;
372 let k = (String::from(fam), bold, italic);
373 let id = match faces.iter().find(|e| e.0 == k) {
374 Some(e) => e.1,
375 None => {
376 let id = ui.rt_face(fam, bold, italic);
377 faces.push((k, id));
378 id
379 }
380 };
381 let pt = if f.size != 0 { f.size } else { base10 } as u32;
382 (id, ui.rt_px(pt * scale / 100))
383 };
384 let indent = self.dp(LEVEL_W) * p.level as i32
385 + if p.list != LS_NONE { self.dp(LIST_W) } else { 0 }
386 + if p.style == PS_QUOTE { self.dp(QUOTE_W) } else { 0 };
387 let avail = (w - indent).max(self.dp(40));
388 let text = p.text();
389 let tb = text.as_bytes();
390 struct Seg {
392 a: usize,
393 b: usize,
394 font: u32,
395 size: i32,
396 asc: i32,
397 desc: i32,
398 lh: i32,
399 fmt: Fmt,
400 }
401 let mut segs: Vec<Seg> = Vec::new();
402 let metrics = |ui: &mut Ui, font: u32, size: i32| {
403 ui.text.set_font(font, size);
404 let (asc, desc) = (ui.text.ascent(), ui.text.descent());
405 (asc, desc, ui.text.line_h().max(asc + desc))
406 };
407 for (a, b, f) in p.spans() {
408 let (font, size) = face(self, f);
409 let (asc, desc, lh) = metrics(self, font, size);
410 segs.push(Seg { a, b, font, size, asc, desc, lh, fmt: f.clone() });
411 }
412 let (mfont, msize) = face(self, segs.first().map_or(&p.mark, |s| &s.fmt));
413 let (masc, mdesc, mlh) = metrics(self, mfont, msize);
414 let tab = self.dp(TAB_W).max(1) << 8;
415 let width = |ui: &mut Ui, a: usize, b: usize, x: i32| -> i32 {
417 let mut pen = x;
418 for s in &segs[segs.partition_point(|s| s.b <= a)..] {
419 if s.a >= b {
420 break;
421 }
422 let (s0, s1) = (s.a.max(a), s.b.min(b));
423 if s0 >= s1 {
424 continue;
425 }
426 ui.text.set_font(s.font, s.size);
427 let mut i = s0;
428 while i < s1 {
429 if tb[i] == b'\t' {
430 pen = (pen / tab + 1) * tab;
431 i += 1;
432 continue;
433 }
434 let j = tb[i..s1].iter().position(|&c| c == b'\t').map_or(s1, |k| i + k);
435 pen += ui.text.measure_n(&tb[i..], j - i);
436 i = j;
437 }
438 }
439 pen - x
440 };
441 let mut rows_ab: Vec<(usize, usize)> = Vec::new();
443 if !text.is_empty() {
444 let ops: Vec<usize> = segment::line_breaks(text).into_iter().map(|o| o.0).collect();
445 let vis = |e: usize, s: usize| {
446 let mut v = e;
447 while v > s && (tb[v - 1] == b' ' || tb[v - 1] == b'\t') {
448 v -= 1;
449 }
450 v
451 };
452 let lim = avail << 8;
453 let mut start = 0;
454 let mut x = 0; let mut prev = 0; for &o in &ops {
457 if o <= prev {
458 continue;
459 }
460 let atom = width(self, prev, vis(o, prev), x);
461 if x + atom > lim && prev > start {
462 rows_ab.push((start, prev));
463 start = prev;
464 x = 0;
465 }
466 if x == 0 {
467 let mut a = start;
469 loop {
470 let w_all = {
472 let ve = vis(o, a);
473 let mut n = 64;
474 loop {
475 let mut pe = (a + n).min(ve);
476 while !text.is_char_boundary(pe) {
477 pe -= 1;
478 }
479 let w = width(self, a, pe, 0);
480 if w > lim || pe >= ve {
481 break w;
482 }
483 n *= 2;
484 }
485 };
486 if w_all <= lim || a >= o {
487 break;
488 }
489 let mut e = segment::grapheme_next(text, a).min(o);
490 while e < o {
491 let n = segment::grapheme_next(text, e).min(o);
492 if width(self, a, n, 0) > lim {
493 break;
494 }
495 e = n;
496 }
497 rows_ab.push((a, e));
498 a = e;
499 start = e;
500 }
501 }
502 x += width(self, prev.max(start), o, x);
503 prev = o;
504 }
505 rows_ab.push((start, text.len()));
506 } else {
507 rows_ab.push((0, 0));
508 }
509 let mut rows: Vec<Row> = Vec::with_capacity(rows_ab.len());
511 let mut y = 0;
512 let nrows = rows_ab.len();
513 for (ri, &(ra, rb)) in rows_ab.iter().enumerate() {
514 let mut pieces: Vec<Piece> = Vec::new();
515 let mut stops: Vec<(usize, i32)> = Vec::new();
516 let (mut asc, mut desc, mut lh) = (0, 0, 0);
517 let mut pen = 0; let justify = p.align == RA_JUSTIFY && ri + 1 < nrows;
519 for s in &segs[segs.partition_point(|s| s.b <= ra)..] {
520 if s.a >= rb {
521 break;
522 }
523 let (s0, s1) = (s.a.max(ra), s.b.min(rb));
524 if s0 >= s1 {
525 continue;
526 }
527 asc = asc.max(s.asc);
528 desc = desc.max(s.desc);
529 lh = lh.max(s.lh);
530 self.text.set_font(s.font, s.size);
531 let mut i = s0;
533 while i < s1 {
534 if tb[i] == b'\t' {
535 let np = (pen / tab + 1) * tab;
536 stops.push((i, (pen + 128) >> 8));
537 pieces.push(Piece { a: i, b: i + 1, x: (pen + 128) >> 8, w: (np - pen + 128) >> 8, font: s.font, size: s.size, asc: s.asc, flags: s.fmt.flags, color: s.fmt.color, bg: s.fmt.bg, link: !s.fmt.link.is_empty(), tab: true });
538 pen = np;
539 i += 1;
540 continue;
541 }
542 let mut j = tb[i..s1].iter().position(|&c| c == b'\t').map_or(s1, |k| i + k);
543 if justify {
544 if let Some(k) = tb[i..j].iter().position(|&c| c == b' ') {
545 j = i + k + 1;
546 }
547 }
548 let piece = &tb[i..j];
549 let x0 = pen;
550 let mut g = i;
552 while g < j {
553 stops.push((g, (x0 + self.text.measure_n(piece, g - i) + 128) >> 8));
554 g = segment::grapheme_next(text, g).min(j);
555 }
556 let w = self.text.measure_n(piece, j - i);
557 pieces.push(Piece { a: i, b: j, x: (x0 + 128) >> 8, w: (w + 255) >> 8, font: s.font, size: s.size, asc: s.asc, flags: s.fmt.flags, color: s.fmt.color, bg: s.fmt.bg, link: !s.fmt.link.is_empty(), tab: false });
558 pen = x0 + w;
559 i = j;
560 }
561 }
562 stops.push((rb, (pen + 128) >> 8));
563 if pieces.is_empty() {
564 asc = masc;
565 desc = mdesc;
566 lh = mlh;
567 }
568 let h = lh.max(asc + desc).max(1);
569 let base = asc + (h - asc - desc) / 2;
570 let mut vis_end = rb;
572 while vis_end > ra && (tb[vis_end - 1] == b' ' || tb[vis_end - 1] == b'\t') {
573 vis_end -= 1;
574 }
575 let vis_w = stops.iter().find(|s| s.0 >= vis_end).map_or(0, |s| s.1);
576 let off = match p.align {
577 RA_CENTER => ((avail - vis_w) / 2).max(0),
578 RA_RIGHT => (avail - vis_w).max(0),
579 _ => 0,
580 };
581 if justify {
582 let blanks: Vec<usize> = (ra..vis_end).filter(|&i| tb[i] == b' ').collect();
583 if !blanks.is_empty() && vis_w < avail {
584 let extra = ((avail - vis_w) as i64) << 8;
585 let n = blanks.len() as i64;
586 let shift = |b: usize| -> i32 {
587 let k = blanks.partition_point(|&s| s < b) as i64;
588 ((extra * k / n) >> 8) as i32
589 };
590 for s in stops.iter_mut() {
591 s.1 += shift(s.0);
592 }
593 for pc in pieces.iter_mut() {
594 pc.x += shift(pc.a);
595 }
596 }
597 }
598 for s in stops.iter_mut() {
599 s.1 += indent + off;
600 }
601 for pc in pieces.iter_mut() {
602 pc.x += indent + off;
603 }
604 stops.dedup_by_key(|s| s.0);
605 rows.push(Row { a: ra, b: rb, y, h, base, stops, pieces });
606 y += h;
607 }
608 let top = match p.style {
609 PS_H1 => self.dp(12),
610 PS_H2 => self.dp(10),
611 PS_H3 => self.dp(8),
612 _ => 0,
613 };
614 let gap = if p.list != LS_NONE { self.dp(3) } else { self.dp(8) };
615 PLay { top, h: top + y + gap, rows, indent, mfont: (mfont, msize, masc) }
616 }
617
618 fn rt_layout(&mut self, v: &mut View, doc: &RichDoc, w: i32) -> (Vec<Rc<PLay>>, Vec<i32>, i32) {
620 let key = LKey { w, dpi: self.wp.rt.dpi, scale: self.layout_scale(), font: doc.font.clone(), size: doc.size, ui: self.style.ts_font()[TS_BODY as usize] as u32 };
621 if key != v.key {
622 v.lays.clear();
623 v.key = key;
624 }
625 let base10 = if doc.size == 0 { 110 } else { doc.size };
626 let prev = self.text.get_font();
627 let mut faces = Vec::new();
628 let mut keep = BTreeMap::new();
629 let mut lays = Vec::with_capacity(doc.para_count());
630 let mut tops = Vec::with_capacity(doc.para_count());
631 let mut y = 0;
632 for p in doc.paras() {
633 let k = (p.version(), p.align, p.list, p.level, p.style);
634 let l = match v.lays.get(&k) {
635 Some(l) => l.clone(),
636 None => Rc::new(self.rt_lay(p, w, &doc.font, base10, &mut faces)),
637 };
638 tops.push(y);
639 y += l.h;
640 keep.insert(k, l.clone());
641 lays.push(l);
642 }
643 v.lays = keep;
644 self.text.set_font(prev.0, prev.1);
645 (lays, tops, y)
646 }
647
648 #[cfg_attr(jfgui_dev, track_caller)]
650 pub fn richtext(&mut self, id: u32, doc: &mut RichDoc, flags: u32) -> u32 {
651 let _src = self.src_scope();
652 let ro = flags & RT_READONLY != 0;
653 let stflags = self.take_flags();
654 let r = self.place(self.dp(NAT_W), self.dp(NAT_H));
655 let hair = self.hair();
656 let st = self.style;
657 let frame = flags & RT_NO_FRAME == 0;
658 let pad = if frame { self.dp(10) } else { self.dp(2) };
659 let sbw = st.scrollbar_thickness(self);
660 let (tx, ty) = (r[0] + pad, r[1] + pad);
661 let tw = (r[2] - pad * 2 - sbw).max(self.dp(40));
662 let th = (r[3] - pad * 2).max(1);
663 let mut v = self.m.rich.views.remove(&id).unwrap_or_default();
664 v.seen = self.frame;
665 let rev0 = doc.revision();
666 let sel0 = doc.selection();
667 let (mut lays, mut tops, mut total) = self.rt_layout(&mut v, doc, tw);
668 let mut hb = HB_FOCUS | HB_TEXT | CUR_IBEAM << HB_CURSOR_SHIFT;
670 if stflags & ST_DISABLED != 0 {
671 hb |= HB_DISABLED;
672 }
673 self.wp.ctx.last_rect = r;
674 let hit0 = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
675 let ev = self.behave(id, hb);
676 let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
677 let focused = !disabled && self.ed_focused(id);
678 let mut out = ev & (EV_HOVER | EV_HELD | EV_DOUBLE | EV_RCLICK | EV_DRAG);
679 if focused {
680 if let (Some(sel), Some(held)) = (self.textbox_selection(id), v.held) {
682 if sel != held {
683 doc.set_selection(TextSel::new(doc.unflat(sel.0), doc.unflat(sel.1)));
684 }
685 }
686 if let Some(t) = self.a11y_text(id) {
687 if !ro {
688 doc.set_paras(RichDoc::from_plain(&t).paras().to_vec());
689 let e = doc.end();
690 doc.set_selection(TextSel::at(e));
691 }
692 }
693 } else if v.held.is_some() {
694 v.held = None;
695 v.drag = 0;
696 self.ed_drop(id);
697 out |= EV_COMMIT;
698 }
699 if doc.revision() != rev0 {
700 (lays, tops, total) = self.rt_layout(&mut v, doc, tw);
701 }
702 let mut laid = doc.revision();
703 let (mx, my) = (self.rtl_map(self.wp.ctx.mx, tx, tw), self.wp.ctx.my);
705 let (cx, cy) = (mx - tx, my - ty + v.scroll);
706 let c = &self.wp.ctx;
707 let press = c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == id;
708 if press && !disabled {
709 let view = RView { doc: &*doc, lays: &lays, tops: &tops };
710 let p = view.hit(cx, cy);
711 let link = view.char_at(cx, cy).and_then(|q| doc.link_at(q).map(String::from));
712 v.prefx = -1;
713 v.t0 = self.now_ms;
714 if let Some(url) = link.filter(|_| ro || self.mods & MOD_CTRL != 0) {
715 self.rt_link_activate(id, &url, flags);
716 v.drag = 0;
717 } else if ev & EV_DOUBLE != 0 {
718 let (a, b) = word_range(doc.para(p.line).text(), p.byte);
719 v.word = (TextPos::new(p.line, a), TextPos::new(p.line, b));
720 doc.set_selection(TextSel::new(v.word.0, v.word.1));
721 v.drag = 2;
722 } else if self.mods & MOD_SHIFT != 0 {
723 let a = doc.selection().anchor;
724 doc.set_selection(TextSel::new(a, p));
725 v.drag = 1;
726 } else {
727 doc.set_selection(TextSel::at(p));
728 v.drag = 1;
729 }
730 } else if ev & EV_HELD != 0 && v.drag != 0 {
731 let lh = lays.first().and_then(|l| l.rows.first()).map_or(self.dp(20), |r| r.h);
732 if my < ty {
733 v.scroll -= lh;
734 } else if my >= ty + th {
735 v.scroll += lh;
736 }
737 v.scroll = v.scroll.clamp(0, (total - th).max(0));
738 let cy = my - ty + v.scroll;
739 let view = RView { doc: &*doc, lays: &lays, tops: &tops };
740 let p = view.hit(cx, cy);
741 if v.drag == 2 {
742 let (a, b) = word_range(doc.para(p.line).text(), p.byte);
743 let (wa, wb) = (TextPos::new(p.line, a), TextPos::new(p.line, b));
744 if wa < v.word.0 {
745 doc.set_selection(TextSel::new(v.word.1, wa));
746 } else {
747 doc.set_selection(TextSel::new(v.word.0, wb.max(v.word.1)));
748 }
749 } else {
750 let a = doc.selection().anchor;
751 doc.set_selection(TextSel::new(a, p));
752 }
753 } else if ev & EV_HELD == 0 {
754 v.drag = 0;
755 }
756 if focused && !v.link_open {
758 loop {
759 if doc.revision() != laid {
760 (lays, tops, total) = self.rt_layout(&mut v, doc, tw);
761 laid = doc.revision();
762 }
763 let k0 = self.ed_key_first();
764 if k0 == 0 {
765 break;
766 }
767 let k = self.rtl_text_key(k0);
768 if self.rt_key(id, doc, &mut v, k, flags) {
769 self.ed_key_take();
770 v.t0 = self.now_ms;
771 if v.link_open {
772 break;
773 }
774 continue;
775 }
776 let act = {
777 let view = RView { doc: &*doc, lays: &lays, tops: &tops };
778 ed_key(&view, doc.selection(), &mut v.prefx, k, th, false)
779 };
780 let Some(act) = act else { break };
781 self.ed_key_take();
782 v.t0 = self.now_ms;
783 match act {
784 EdAct::Sel(s) => doc.set_selection(s),
785 EdAct::Del(a, b, _) if !ro => {
786 let back = k & 0xFFFF == VK_BACK;
787 doc.delete_kind(a, b, if !doc.selection().is_empty() { 0 } else if back { RK_BACK } else { RK_DEL });
788 }
789 EdAct::Enter if !ro => {
790 doc.split();
791 }
792 EdAct::Copy => self.rt_copy(doc),
793 EdAct::Cut => {
794 self.rt_copy(doc);
795 if !ro {
796 doc.delete_selection();
797 }
798 }
799 EdAct::Paste if !ro => self.rt_paste(doc, flags & RT_PLAIN_PASTE != 0),
800 EdAct::Undo if !ro => {
801 doc.undo();
802 }
803 EdAct::Redo if !ro => {
804 doc.redo();
805 }
806 _ => {}
807 }
808 }
809 let cps = self.text_take(super::ctx::TEXTQ_MAX);
810 if !ro {
811 let s = typed_text(&cps, false);
812 if !s.is_empty() {
813 doc.insert_text_kind(&s, RK_TYPE);
814 v.t0 = self.now_ms;
815 v.prefx = -1;
816 }
817 }
818 }
819 if doc.revision() != laid || lays.len() != doc.para_count() {
820 (lays, tops, total) = self.rt_layout(&mut v, doc, tw);
821 }
822 let changed = doc.revision() != rev0;
823 let moved = doc.selection() != sel0;
824 if changed {
825 out |= EV_ACT;
826 }
827 if changed || moved {
828 self.repass();
829 }
830 let lh = lays.first().and_then(|l| l.rows.first()).map_or(self.dp(20), |r| r.h);
832 let wy = self.wp.ctx.wheel_y;
833 if (ev & EV_HOVER != 0 || self.wp.ctx.hot == id.wrapping_add(1)) && wy != 0 {
834 let s = (v.scroll - wy * lh * 3 / 120).clamp(0, (total - th).max(0));
835 if s != v.scroll {
836 v.scroll = s;
837 self.wp.ctx.wheel_y = 0;
838 }
839 }
840 let (_, pan) = self.pan_take(id);
842 v.scroll = (v.scroll + pan).clamp(0, (total - th).max(0));
843 let caret = doc.clamp(doc.selection().caret);
844 let (crow_y, crow_h, cxp) = {
845 let view = RView { doc: &*doc, lays: &lays, tops: &tops };
846 let row = row_of(&view, caret);
847 let (y, h) = view.row_y(caret.line, row);
848 (y, h, view.x_at(caret.line, row, caret.byte))
849 };
850 if focused && (doc.selection() != v.last_sel || doc.revision() != v.last_rev) {
851 if crow_y < v.scroll {
852 v.scroll = crow_y;
853 }
854 if crow_y + crow_h > v.scroll + th {
855 v.scroll = crow_y + crow_h - th;
856 }
857 }
858 v.last_sel = doc.selection();
859 v.last_rev = doc.revision();
860 v.scroll = v.scroll.clamp(0, (total - th).max(0));
861
862 if frame {
864 let mut sp = Sp { value: NAT_W, value2: lh, flags: 1, ..Sp::default() };
865 let _ = st.textbox_metrics(self, &mut sp);
866 sp.place(r);
867 sp.ev = ev;
868 sp.value = focused as i32;
869 sp.value2 = disabled as i32;
870 st.textbox_frame(self, &sp);
871 }
872 self.clip_push(r[0] + hair, r[1] + hair, (r[2] - hair * 2).max(0), (r[3] - hair * 2).max(0));
873 self.ltr_begin(tx, tw);
874 let marks = list_marks(doc.paras());
875 let (sa, sb) = doc.selection().range();
876 let sel_col = if focused {
877 (self.color(T_ACCENT) & 0x00FF_FFFF) | 0x60 << 24
878 } else {
879 (self.color(T_TEXT3) & 0x00FF_FFFF) | 0x38 << 24
880 };
881 let tcol = self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT });
882 let qcol = self.color(T_TEXT2);
883 let lcol = self.color(T_ACCENT_TEXT);
884 let hc = self.high_contrast;
885 let first = tops.partition_point(|&t| t <= v.scroll).saturating_sub(1);
886 let mut links: Vec<([i32; 4], String)> = Vec::new();
887 for line in first..lays.len() {
888 let top = ty + tops[line] - v.scroll;
889 if top > ty + th {
890 break;
891 }
892 let l = &lays[line];
893 let p = doc.para(line);
894 let ptop = top + l.top;
895 if p.style == PS_QUOTE {
897 let x = tx + self.dp(LEVEL_W) * p.level as i32 + self.dp(4);
898 let hgt = l.rows.last().map_or(0, |r| r.y + r.h);
899 self.draw_color(self.color(T_CTRL_STROKE2) | 0xFF00_0000);
900 self.draw_rect(x, ptop, self.dp(3), hgt);
901 }
902 if let (Some(m), Some(row)) = (&marks[line], l.rows.first()) {
903 let (mf, ms, masc) = l.mfont;
904 let c = match p.runs().next() {
905 Some((_, f)) if f.color != 0 && !hc => opaque(f.color),
906 _ => tcol,
907 };
908 let base = ptop + row.y + row.base;
909 if p.list == LS_BULLET {
910 let d = ((masc as f32 * 0.36) as i32).max(self.dp(4)) & !1;
912 let cx = tx + l.indent - self.dp(9) - d / 2;
913 let cy = base - (masc as f32 * 0.34) as i32;
914 self.draw_color(c);
915 match p.level % 3 {
916 0 => self.draw_circle(cx << 8, cy << 8, (d << 8) / 2, 0),
917 1 => self.draw_circle(cx << 8, cy << 8, (d << 8) / 2 - 128, self.hair().max(1) << 8),
918 _ => self.draw_rect(cx - d / 2, cy - d / 2, d, d),
919 }
920 } else {
921 self.text.set_font(mf, ms);
922 let mw = self.text.width(m);
923 let x = tx + l.indent - self.dp(6) - mw;
924 self.cmd_text(x, base.max(ptop + row.y + masc), m.as_bytes(), mf, ms, c, 0);
925 }
926 }
927 for row in &l.rows {
928 let ry = ptop + row.y;
929 if ry > ty + th || ry + row.h < ty {
930 continue;
931 }
932 let (rs, re) = (TextPos::new(line, row.a), TextPos::new(line, row.b));
935 if sa < sb && sa <= re && sb >= rs {
936 let (lo, hi) = (sa.max(rs), sb.min(re));
937 let brk = sb > re && core::ptr::eq(row, l.rows.last().unwrap());
938 if lo < hi || brk {
939 let x0 = row.x(lo.byte);
940 let x1 = row.x(hi.byte) + if brk { self.dp(5) } else { 0 };
941 self.draw_color(sel_col);
942 self.draw_rect(tx + x0, ry, (x1 - x0).max(1), row.h);
943 }
944 }
945 for pc in &row.pieces {
946 let x = tx + pc.x;
947 let (pcolor, pbg) = if hc { (0, 0) } else { (pc.color, pc.bg) };
950 if pbg != 0 {
951 self.draw_color(opaque(pbg));
952 self.draw_rect(x, ry, pc.w, row.h);
953 } else if hc && pc.bg != 0 {
954 self.draw_color(tcol);
955 self.draw_frame(x, ry, pc.w, row.h, 0, hair.max(1));
956 }
957 if pc.tab {
958 continue;
959 }
960 let col = if disabled {
961 tcol
962 } else if pcolor != 0 {
963 opaque(pcolor)
964 } else if pbg != 0 {
965 let c = opaque(pbg);
967 let luma = ((c >> 16 & 255) * 299 + (c >> 8 & 255) * 587 + (c & 255) * 114) / 1000;
968 if luma > 140 { 0xFF1A1A1A } else { 0xFFF2F2F2 }
969 } else if pc.link {
970 lcol
971 } else if p.style == PS_QUOTE {
972 qcol
973 } else {
974 tcol
975 };
976 let base = ry + row.base;
977 self.cmd_text(x, base, &p.text().as_bytes()[pc.a..pc.b], pc.font, pc.size, col, 0);
978 let px = pc.size >> 8;
979 let th_line = (px / 14).max(1);
980 if pc.flags & RF_UNDER != 0 || pc.link {
981 self.draw_color(col);
982 self.draw_rect(x, base + (px / 9).max(1), pc.w, th_line);
983 }
984 if pc.flags & RF_STRIKE != 0 {
985 self.draw_color(col);
986 self.draw_rect(x, base - pc.asc * 3 / 10, pc.w, th_line);
987 }
988 if pc.link && links.len() < 64 {
989 let t = String::from(&p.text()[pc.a..pc.b]);
990 match links.last_mut() {
991 Some((lr, lt)) if lr[1] == ry && lr[0] + lr[2] == x => {
992 lr[2] += pc.w;
993 lt.push_str(&t);
994 }
995 _ => links.push(([x, ry, pc.w, row.h], t)),
996 }
997 }
998 }
999 }
1000 }
1001 let (crx, cry) = (tx + cxp, ty + crow_y - v.scroll);
1003 v.caret_rect = [crx, cry, hair.max(1), crow_h];
1004 if focused {
1005 let pe = self.ed_preedit();
1006 if !pe.text.is_empty() {
1007 let l = &lays[caret.line];
1009 let row = &l.rows[row_of(&RView { doc: &*doc, lays: &lays, tops: &tops }, caret)];
1010 let pc = row.pieces.iter().find(|pc| pc.a <= caret.byte && caret.byte <= pc.b && !pc.tab);
1011 let font = pc.map_or((l.mfont.0, l.mfont.1), |pc| (pc.font, pc.size));
1012 let bg = if frame { self.color(if focused { T_INPUT_ACTIVE } else { T_CTRL }) } else { self.color(T_BG) };
1013 let a = (bg >> 24) as i32;
1014 let bg = crate::theme::color_mix(self.color(T_BG) | 0xFF00_0000, bg | 0xFF00_0000, a + (a >> 7));
1015 self.ed_preedit_draw(&pe, crx, cry, crow_h, cry + row.base, font, tcol, bg);
1016 } else if doc.selection().is_empty() && self.ed_caret_on(v.t0) && cry + crow_h > ty && cry < ty + th {
1017 self.draw_color(tcol);
1018 self.draw_rect(crx, cry, hair.max(1), crow_h);
1019 }
1020 plat::ime_inline(true); self.ime_pos(crx, cry, crow_h);
1022 }
1023 self.ltr_end();
1024 for (lr, label) in &links {
1026 self.wp.ctx.last_rect = *lr;
1027 let before = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
1028 self.hit(id, hb & ((1 << HB_CURSOR_SHIFT) - 1) | CUR_HAND << HB_CURSOR_SHIFT);
1029 if let Some(h) = self.wp.ctx.hits[self.wp.ctx.hit_cur].get_mut(before) {
1030 h.continuation = true;
1031 }
1032 self.wp.ctx.last_state = 0;
1033 self.sem(K_LINK, 0, Some(label), 0);
1034 }
1035 self.clip_pop();
1036 if total > th {
1038 let mut pos = v.scroll;
1039 self.next_rect_px(r[0] + r[2] - sbw - hair, r[1] + hair, sbw, r[3] - hair * 2);
1040 if self.scrollbar(id.wrapping_add(1), &mut pos, total, th, true) & EV_ACT != 0 {
1041 v.scroll = pos.clamp(0, (total - th).max(0));
1042 }
1043 }
1044 self.pan_view(id, hit0, super::touch::AX_V, [0, v.scroll], [0, (total - th).max(0)]);
1046 if v.label.0 != doc.uid() || v.label.1 != doc.revision() {
1048 v.label = (doc.uid(), doc.revision(), doc.text());
1049 }
1050 let (fa, fc) = (doc.flat(doc.selection().anchor), doc.flat(doc.selection().caret));
1051 self.wp.ctx.last_rect = r;
1052 self.wp.ctx.last_state = if focused { SS_FOCUSED as u32 } else { 0 } | if disabled { SS_DISABLED as u32 } else { 0 };
1053 let kind = (TB_MULTI | if ro { TB_READONLY } else { 0 }) >> 24;
1054 let label = core::mem::take(&mut v.label.2);
1055 {
1056 let s = self.sem(K_TEXTBOX, id, Some(&label), if focused { fc as i32 } else { 0 });
1057 s.max = if focused { fa as i32 } else { 0 };
1058 s.min = kind as i32;
1059 }
1060 v.label.2 = label;
1061 if focused {
1062 self.ed_hold(id, fa, fc);
1063 v.held = Some((fa, fc));
1064 out |= EV_FOCUS;
1065 }
1066 if v.link_open {
1068 self.rt_link_flyout(id, doc, &mut v);
1069 }
1070 self.m.rich.views.insert(id, v);
1071 let frame_now = self.frame;
1073 if self.m.rich.views.len() > 8 {
1074 self.m.rich.views.retain(|_, v| frame_now.wrapping_sub(v.seen) < 600);
1075 }
1076 self.wp.ctx.last_rect = r;
1077 out
1078 }
1079
1080 fn rt_key(&mut self, id: u32, doc: &mut RichDoc, v: &mut View, k: u32, flags: u32) -> bool {
1082 let ro = flags & RT_READONLY != 0;
1083 let vk = k & 0xFFFF;
1084 let m = k >> 16;
1085 let altgr = m & MOD_ALTGR != 0 || m & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT;
1086 if altgr || m & MOD_ALT != 0 {
1087 return false;
1088 }
1089 let ctrl = m & MOD_CTRL != 0;
1090 let shift = m & MOD_SHIFT != 0;
1091 let sel = doc.selection();
1092 if ctrl {
1093 let edit = |doc: &mut RichDoc, f: &dyn Fn(&mut RichDoc) -> bool| {
1094 if !ro {
1095 f(doc);
1096 }
1097 true
1098 };
1099 return match (vk, shift) {
1100 (0x42, false) => edit(doc, &|d| d.toggle(RF_BOLD)),
1101 (0x49, false) => edit(doc, &|d| d.toggle(RF_ITALIC)),
1102 (0x55, false) => edit(doc, &|d| d.toggle(RF_UNDER)),
1103 (0x58, true) => edit(doc, &|d| d.toggle(RF_STRIKE)),
1104 (VK_SPACE, false) => edit(doc, &|d| d.clear_format()),
1105 (0x4C, false) => edit(doc, &|d| d.set_align(RA_LEFT)),
1106 (0x45, false) => edit(doc, &|d| d.set_align(RA_CENTER)),
1107 (0x52, false) => edit(doc, &|d| d.set_align(RA_RIGHT)),
1108 (0x4A, false) => edit(doc, &|d| d.set_align(RA_JUSTIFY)),
1109 (0x38, true) | (0x4C, true) => edit(doc, &|d| d.toggle_list(LS_BULLET)),
1110 (0x37, true) => edit(doc, &|d| d.toggle_list(LS_NUMBER)),
1111 (0x31, false) => edit(doc, &|d| d.set_style(PS_H1)),
1112 (0x32, false) => edit(doc, &|d| d.set_style(PS_H2)),
1113 (0x33, false) => edit(doc, &|d| d.set_style(PS_H3)),
1114 (0x30, false) => edit(doc, &|d| d.set_style(PS_NORMAL)),
1115 (VK_OEM_6, false) | (VK_OEM_4, false) => {
1116 let up = vk == VK_OEM_6;
1117 let base = if doc.size == 0 { 110 } else { doc.size };
1118 let cur = doc.current_fmt().size;
1119 let cur = if cur == 0 { base } else { cur };
1120 let next = if up { SIZES.iter().copied().find(|&s| s > cur).unwrap_or(cur) } else { SIZES.iter().rev().copied().find(|&s| s < cur).unwrap_or(cur) };
1121 edit(doc, &|d| d.set_size(next))
1122 }
1123 (0x4B, false) => {
1124 if !ro {
1125 self.rt_link_open(id, doc, v);
1126 }
1127 true
1128 }
1129 (0x56, true) => {
1130 if !ro {
1131 self.rt_paste(doc, true);
1132 }
1133 true
1134 }
1135 (VK_RETURN, false) => {
1136 if let Some(url) = doc.link_at(sel.caret).or_else(|| if sel.caret.byte > 0 { doc.link_at(TextPos::new(sel.caret.line, sel.caret.byte - 1)) } else { None }).map(String::from) {
1138 self.rt_link_activate(id, &url, flags);
1139 return true;
1140 }
1141 false
1142 }
1143 _ => false,
1144 };
1145 }
1146 match vk {
1147 VK_TAB => {
1148 let para = doc.current_para();
1149 let in_list = para.list != LS_NONE || !sel.is_empty() && (sel.start().line != sel.end().line);
1150 if in_list || (shift && para.level > 0) {
1151 if !ro {
1152 doc.indent(if shift { -1 } else { 1 });
1153 }
1154 return true;
1155 }
1156 if flags & RT_TAB != 0 && !shift {
1157 if !ro {
1158 doc.insert_text_kind("\t", RK_TYPE);
1159 }
1160 return true;
1161 }
1162 false
1163 }
1164 VK_BACK if sel.is_empty() && sel.caret.byte == 0 && !ro => {
1165 let para = doc.current_para();
1166 if para.list != LS_NONE {
1167 doc.set_list(LS_NONE);
1168 return true;
1169 }
1170 if para.level > 0 {
1171 doc.indent(-1);
1172 return true;
1173 }
1174 false
1175 }
1176 VK_ESCAPE => {
1177 self.wp.ctx.focus = 0;
1178 self.repass();
1179 true
1180 }
1181 _ => false,
1182 }
1183 }
1184
1185 fn rt_copy(&mut self, doc: &RichDoc) {
1187 let sel = doc.selection();
1188 if sel.is_empty() {
1189 return;
1190 }
1191 let frag = doc.selected_paras();
1192 let text = if frag.len() > 1 { RichDoc::from_paras(frag.clone()).to_plain() } else { doc.selected_text() };
1193 let html = clip_html(&frag, &doc.font, doc.size);
1194 self.clip_set_many(&[(CLIP_TEXT, text.as_bytes()), (CLIP_HTML, html.as_bytes())]);
1195 }
1196
1197 fn rt_paste(&mut self, doc: &mut RichDoc, plain: bool) {
1199 if !plain {
1200 if let Some(h) = self.clip_get(CLIP_HTML) {
1201 let html = String::from_utf8_lossy(&h).into_owned();
1202 let d = RichDoc::from_html(&html);
1203 if d.paras().iter().any(|p| !p.is_empty()) || d.para_count() > 1 {
1204 doc.insert_paras(d.paras());
1205 return;
1206 }
1207 }
1208 }
1209 if let Some(t) = self.clip_get(CLIP_TEXT) {
1210 let s = String::from_utf8_lossy(&t).replace('\0', "");
1211 if !s.is_empty() {
1212 doc.insert_text_kind(&s, 0);
1213 }
1214 }
1215 }
1216
1217 fn rt_link_activate(&mut self, id: u32, url: &str, flags: u32) {
1218 self.m.rich.link = Some((id, String::from(url)));
1219 if flags & RT_OPEN_LINKS != 0 {
1220 self.open_doc_link(url);
1221 }
1222 self.repass();
1223 }
1224
1225 pub fn richtext_link(&mut self, id: u32) -> Option<String> {
1228 match &self.m.rich.link {
1229 Some((i, _)) if *i == id => self.m.rich.link.take().map(|l| l.1),
1230 _ => None,
1231 }
1232 }
1233
1234 fn rt_link_open(&mut self, id: u32, doc: &RichDoc, v: &mut View) {
1236 let sel = doc.selection();
1237 let at = if sel.is_empty() { sel.caret } else { sel.start() };
1238 let cur = doc.link_at(at).or_else(|| if at.byte > 0 { doc.link_at(TextPos::new(at.line, at.byte - 1)) } else { None });
1239 v.link_text = String::from(cur.unwrap_or("https://"));
1240 v.link_open = true;
1241 self.popup_open(id.wrapping_add(2));
1242 self.wp.ctx.focus = id.wrapping_add(3);
1243 self.wp.ctx.focus_visible = false;
1244 self.repass();
1245 }
1246
1247 #[cfg_attr(jfgui_dev, track_caller)]
1251 pub fn richtext_link_dialog(&mut self, id: u32, doc: &RichDoc) {
1252 let _src = self.src_scope();
1253 let mut v = self.m.rich.views.remove(&id).unwrap_or_default();
1254 self.rt_link_open(id, doc, &mut v);
1255 self.m.rich.views.insert(id, v);
1256 }
1257
1258 fn rt_link_flyout(&mut self, id: u32, doc: &mut RichDoc, v: &mut View) {
1259 let pid = id.wrapping_add(2);
1260 if self.wp.ctx.focus == id.wrapping_add(3) && self.ed_key_chord(VK_ESCAPE, 0) {
1262 v.link_open = false;
1263 self.popup_close();
1264 self.ed_focus_quiet(id);
1265 return;
1266 }
1267 self.wp.ctx.last_rect = v.caret_rect;
1268 if !self.popup_is_open(pid) {
1269 self.popup_open(pid);
1270 }
1271 let mut done = None;
1272 if self.flyout_begin(pid, 0) {
1273 self.text(TS_BODY, tr("Link-Adresse"));
1274 self.next_size(self.dp(280), 0);
1275 if self.wp.ctx.focus == id.wrapping_add(3) && self.ed_key_chord(VK_RETURN, 0) {
1277 done = Some(true);
1278 }
1279 self.textbox(id.wrapping_add(3), Some("https://"), &mut v.link_text, 2048);
1280 self.box_begin(0, BOX_H, 6);
1281 if self.accent_button(id.wrapping_add(4), tr("OK")) & EV_ACT != 0 {
1282 done = Some(true);
1283 }
1284 if self.button(id.wrapping_add(5), tr("Link entfernen")) & EV_ACT != 0 {
1285 done = Some(false);
1286 }
1287 self.box_end();
1288 } else if self.wp.ctx.focus != id.wrapping_add(3) {
1289 v.link_open = false;
1291 }
1292 self.flyout_end();
1293 if let Some(set) = done {
1294 let url = if set { String::from(v.link_text.trim()) } else { String::new() };
1295 doc.set_link(&url);
1296 v.link_open = false;
1297 self.popup_close();
1298 self.ed_focus_quiet(id);
1299 }
1300 }
1301
1302 fn rt_art(&mut self, art: &'static str, color: u32, accent: u32) -> Rc<Art> {
1306 let k = (art.as_ptr() as usize, color, accent);
1307 self.m.rich.icons.entry(k).or_insert_with(|| Rc::new(Art { art, color, accent })).clone()
1308 }
1309
1310 fn rt_tool(&mut self, id: u32, art: &'static str, label: &str, checked: bool, enabled: bool, accent: u32) -> bool {
1312 let st = self.style;
1313 let mut flags = ST_SUBTLE | ST_ICON_ONLY;
1314 if checked {
1315 flags |= ST_CHECKED;
1316 }
1317 if !enabled {
1318 flags |= ST_DISABLED;
1319 }
1320 let s = self.dp(32);
1321 let r = self.place(s, s);
1322 let ev = self.behave(id, if enabled { HB_FOCUS } else { HB_DISABLED });
1323 let mut sp = Sp { flags, label: Some(label), ..Sp::default() };
1324 self.sp_placed(&mut sp, ev);
1325 st.button(self, &sp);
1326 let c = self.color(if enabled { T_TEXT } else { T_TEXT_DISABLED });
1327 let a = if accent != 0 { opaque(accent) } else { c };
1328 let art = self.rt_art(art, c, a);
1329 let is = self.dp(18);
1330 self.cmd_custom(r[0] + (r[2] - is) / 2, r[1] + (r[3] - is) / 2, is, is, Some(art), 0, 0);
1331 self.focus_ring_draw(ev);
1332 if checked {
1333 self.wp.ctx.last_state |= SS_CHECKED as u32;
1334 }
1335 self.sem(K_BUTTON, id, Some(label), 0);
1336 self.tooltip(label);
1337 ev & EV_ACT != 0
1338 }
1339
1340 fn rt_drop(&mut self, id: u32, pid: u32, label: &str, w: i32, tip: &str) -> bool {
1342 self.next_size(w, 0);
1343 self.next_flags(ST_SUBTLE);
1344 let ev = self.button(id, label);
1345 self.tooltip(tip);
1346 if ev & EV_ACT != 0 {
1347 self.popup_open(pid);
1348 }
1349 self.flyout_begin(pid, 0)
1350 }
1351
1352 #[cfg_attr(jfgui_dev, track_caller)]
1355 pub fn richtext_toolbar(&mut self, id: u32, editor: u32, doc: &mut RichDoc, groups: u32) -> u32 {
1356 let _src = self.src_scope();
1357 let rev = doc.revision();
1358 let fmt = doc.current_fmt();
1359 let para = doc.current_para().clone();
1360 let mut acted = false;
1361 let id = |k: u32| id.wrapping_add(k);
1362 self.box_begin(0, BOX_H, 2);
1363 let sep = |ui: &mut Ui| {
1364 ui.next_size(ui.dp(1), ui.dp(24));
1365 let r = ui.place(ui.dp(1), ui.dp(24));
1366 ui.draw_color(ui.color(T_DIVIDER));
1367 ui.draw_rect(r[0], r[1] + ui.dp(4), r[2].max(1), r[3] - ui.dp(8));
1368 };
1369 if groups & RTB_HISTORY != 0 {
1370 if self.rt_tool(id(0), ART_UNDO, tr("Rückgängig"), false, doc.can_undo(), 0) {
1371 doc.undo();
1372 acted = true;
1373 }
1374 if self.rt_tool(id(1), ART_REDO, tr("Wiederholen"), false, doc.can_redo(), 0) {
1375 doc.redo();
1376 acted = true;
1377 }
1378 sep(self);
1379 }
1380 if groups & RTB_STYLE != 0 {
1381 let names = [tr("Fließtext"), tr("Überschrift 1"), tr("Überschrift 2"), tr("Überschrift 3"), tr("Zitat")];
1382 if self.rt_drop(id(2), id(3), names[para.style.min(4) as usize], self.dp(118), tr("Formatvorlage")) {
1383 for (i, n) in names.iter().enumerate() {
1384 if para.style == i as u8 {
1385 self.next_flags(ST_CHECKED);
1386 }
1387 if self.menu_item(id(10 + i as u32), n) & EV_ACT != 0 {
1388 doc.set_style(i as u8);
1389 acted = true;
1390 }
1391 }
1392 }
1393 self.flyout_end();
1394 let mut fams: Vec<String> = alloc::vec![String::new()];
1395 fams.extend(self.font_registered());
1396 if !fams.iter().any(|f| f.eq_ignore_ascii_case("monospace")) {
1397 fams.push(String::from("monospace"));
1398 }
1399 let fam_label = if fmt.font.is_empty() { String::from(tr("Standardschrift")) } else { fmt.font.clone() };
1400 if self.rt_drop(id(4), id(5), &fam_label, self.dp(130), tr("Schriftart")) {
1401 for (i, f) in fams.iter().enumerate().take(16) {
1402 if *f == fmt.font {
1403 self.next_flags(ST_CHECKED);
1404 }
1405 let l = if f.is_empty() { String::from(tr("Standardschrift")) } else { f.clone() };
1406 if self.menu_item(id(60 + i as u32), &l) & EV_ACT != 0 {
1407 doc.set_font(f);
1408 acted = true;
1409 }
1410 }
1411 }
1412 self.flyout_end();
1413 let size = if fmt.size == 0 { if doc.size == 0 { 110 } else { doc.size } } else { fmt.size };
1414 if self.rt_drop(id(6), id(7), &size_text(size), self.dp(52), tr("Schriftgröße")) {
1415 for (i, s) in SIZES.iter().enumerate() {
1416 if *s == size {
1417 self.next_flags(ST_CHECKED);
1418 }
1419 let l = size_text(*s);
1420 if self.menu_item(id(80 + i as u32), &l) & EV_ACT != 0 {
1421 doc.set_size(*s);
1422 acted = true;
1423 }
1424 }
1425 }
1426 self.flyout_end();
1427 sep(self);
1428 }
1429 if groups & RTB_CHARS != 0 {
1430 for (k, flag, art, label) in [
1431 (8, RF_BOLD, ART_BOLD, tr("Fett")),
1432 (9, RF_ITALIC, ART_ITALIC, tr("Kursiv")),
1433 (15, RF_UNDER, ART_UNDER, tr("Unterstrichen")),
1434 (16, RF_STRIKE, ART_STRIKE, tr("Durchgestrichen")),
1435 ] {
1436 if self.rt_tool(id(k), art, label, fmt.flags & flag != 0, true, 0) {
1437 doc.toggle(flag);
1438 acted = true;
1439 }
1440 }
1441 sep(self);
1442 }
1443 if groups & RTB_COLOR != 0 {
1444 for (k, pid, base, art, label, cur, is_bg) in [
1446 (17u32, 18u32, 100u32, ART_COLOR, tr("Textfarbe"), fmt.color, false),
1447 (19, 36, 130, ART_HILITE, tr("Hervorhebung"), fmt.bg, true),
1448 ] {
1449 let shown = if cur != 0 { cur } else if is_bg { 0xFFFFE36B } else { self.color(T_TEXT) };
1450 if self.rt_tool(id(k), art, label, false, true, shown) {
1451 self.popup_open(id(pid));
1452 }
1453 if self.flyout_begin(id(pid), 0) {
1454 let auto = if is_bg { tr("Keine") } else { tr("Automatisch") };
1455 if self.menu_item(id(base - 1), auto) & EV_ACT != 0 {
1456 if is_bg { doc.set_bg(0) } else { doc.set_color(0) };
1457 acted = true;
1458 }
1459 let cols: &[u32] = if is_bg { &PALETTE[4..] } else { &PALETTE[..15] };
1460 for (r, row) in cols.chunks(5).enumerate() {
1461 self.box_begin(0, BOX_H, 4);
1462 for (k, &c) in row.iter().enumerate() {
1463 if self.swatch(id(base + (r * 5 + k) as u32), c, c == cur) & EV_ACT != 0 {
1464 if is_bg { doc.set_bg(c) } else { doc.set_color(c) };
1465 acted = true;
1466 self.popup_close();
1467 }
1468 }
1469 self.box_end();
1470 }
1471 }
1472 self.flyout_end();
1473 }
1474 sep(self);
1475 }
1476 if groups & RTB_LINK != 0 {
1477 let on = !fmt.link.is_empty();
1478 if self.rt_tool(id(21), ART_LINK, tr("Link"), on, true, 0) {
1479 let mut v = self.m.rich.views.remove(&editor).unwrap_or_default();
1480 self.rt_link_open(editor, doc, &mut v);
1481 self.m.rich.views.insert(editor, v);
1482 }
1483 sep(self);
1484 }
1485 if groups & RTB_ALIGN != 0 {
1486 for (k, a, art, label) in [
1487 (22, RA_LEFT, ART_LEFT, tr("Linksbündig")),
1488 (23, RA_CENTER, ART_CENTER, tr("Zentriert")),
1489 (24, RA_RIGHT, ART_RIGHT, tr("Rechtsbündig")),
1490 (25, RA_JUSTIFY, ART_JUSTIFY, tr("Blocksatz")),
1491 ] {
1492 if self.rt_tool(id(k), art, label, para.align == a, true, 0) {
1493 doc.set_align(a);
1494 acted = true;
1495 }
1496 }
1497 sep(self);
1498 }
1499 if groups & RTB_LISTS != 0 {
1500 if self.rt_tool(id(26), ART_BULLETS, tr("Aufzählung"), para.list == LS_BULLET, true, 0) {
1501 doc.toggle_list(LS_BULLET);
1502 acted = true;
1503 }
1504 if self.rt_tool(id(27), ART_NUMBERS, tr("Nummerierung"), para.list == LS_NUMBER, true, 0) {
1505 doc.toggle_list(LS_NUMBER);
1506 acted = true;
1507 }
1508 if self.rt_tool(id(28), ART_OUTDENT, tr("Einzug verkleinern"), false, para.level > 0, 0) {
1509 doc.indent(-1);
1510 acted = true;
1511 }
1512 if self.rt_tool(id(29), ART_INDENT, tr("Einzug vergrößern"), false, para.level < LEVEL_MAX, 0) {
1513 doc.indent(1);
1514 acted = true;
1515 }
1516 sep(self);
1517 }
1518 if groups & RTB_CLEAR != 0 && self.rt_tool(id(30), ART_CLEAR, tr("Format löschen"), false, true, 0) {
1519 doc.clear_format();
1520 acted = true;
1521 }
1522 self.box_end();
1523 if acted {
1524 self.ed_focus_quiet(editor);
1525 self.repass();
1526 }
1527 if doc.revision() != rev { EV_ACT } else { 0 }
1528 }
1529}