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