format_text/
format_text.rs

1// Contributed by @mvasi90 https://github.com/mvasi90
2
3use fltk::{
4    app::{App, Scheme},
5    button::Button,
6    enums::{Color, Event, Font},
7    group::*,
8    menu::Choice,
9    misc::Spinner,
10    prelude::*,
11    text::{StyleTableEntry, TextAttr, TextBuffer, TextEditor},
12    window::Window,
13};
14use std::{cell::RefCell, char, rc::Rc};
15
16struct Style {
17    style_table: Vec<StyleTableEntry>,
18}
19
20impl Style {
21    fn new() -> Style {
22        Style {
23            style_table: Vec::<StyleTableEntry>::new(),
24        }
25    }
26
27    // Apply the given style to given text editor.
28    // If the style not exists, then it is created
29    // Any unused style is removed from the style table to keep it compact
30    #[allow(clippy::too_many_arguments)]
31    fn apply_style(
32        &mut self,
33        pos: Option<i32>,
34        ins_items: Option<i32>,
35        del_items: Option<i32>,
36        repl_start: Option<i32>,
37        repl_end: Option<i32>,
38        font: Font,
39        size: i32,
40        color: Color,
41        attr: TextAttr,
42        text_editor: &mut TextEditor,
43    ) {
44        let mut style_buffer = text_editor.style_buffer().unwrap_or_default();
45
46        // get existent style or create new one
47        let style_char =
48            match self.style_table.iter().position(|s| {
49                s.font == font && s.size == size && s.color == color && s.attr == attr
50            }) {
51                Some(i) => ((i + 65) as u8 as char).to_string(),
52                None => {
53                    self.style_table.push(StyleTableEntry {
54                        color,
55                        font,
56                        size,
57                        attr,
58                        bgcolor: Color::Black,
59                    });
60                    ((self.style_table.len() + 64) as u8 as char).to_string()
61                }
62            };
63
64        // insert, delete or replace char index style to the style_buffer
65        match ins_items {
66            Some(n) if n > 0 => {
67                // insert items with style
68                style_buffer.insert(pos.unwrap(), style_char.repeat(n as usize).as_str());
69            }
70            _ => match del_items {
71                Some(n) if n > 0 => {
72                    // delete items with style
73                    style_buffer.remove(pos.unwrap(), pos.unwrap() + n);
74                }
75                _ => match repl_end {
76                    Some(n) if n > 0 => {
77                        // replace items style
78                        style_buffer.replace(
79                            repl_start.unwrap(),
80                            repl_end.unwrap(),
81                            style_char
82                                .repeat((repl_end.unwrap() - repl_start.unwrap()) as usize)
83                                .as_str(),
84                        );
85                    }
86                    _ => {}
87                },
88            },
89        }
90
91        // compact styles on the buffer and reorganize the char index on the style_buffer
92        let mut style_index = style_buffer
93            .text()
94            .chars()
95            .map(|c| (c as usize) - 65)
96            .collect::<Vec<usize>>();
97        style_index.sort_unstable();
98        style_index.dedup();
99        for (i, &v) in style_index.iter().enumerate() {
100            self.style_table.swap(i, v);
101            style_buffer.set_text(
102                style_buffer
103                    .text()
104                    .replace(
105                        (v + 65) as u8 as char,
106                        ((i + 65) as u8 as char).to_string().as_str(),
107                    )
108                    .as_str(),
109            );
110        }
111
112        // remove unused indexes
113        //self.style_table = self.style_table.drain(in_buff.len()..).collect();
114        self.style_table.truncate(style_index.len());
115        text_editor.set_highlight_data(style_buffer, self.style_table.to_owned());
116
117        // uncomment this line to see that the style_table is compact
118        // println!("total styles: {}", self.style_table.len());
119    }
120}
121
122fn main() {
123    let style = Rc::from(RefCell::from(Style::new()));
124
125    let app = App::default().with_scheme(Scheme::Gleam);
126    let mut wind = Window::default()
127        .with_size(500, 200)
128        .with_label("Highlight");
129    let mut vpack = Pack::new(4, 4, 492, 192, "");
130    vpack.set_spacing(4);
131    let mut text_editor = TextEditor::default().with_size(492, 163);
132
133    let mut hpack = Pack::new(4, 4, 492, 25, "").with_type(PackType::Horizontal);
134    hpack.set_spacing(8);
135    let mut font = Choice::default().with_size(130, 25);
136    let mut choice = Choice::default().with_size(130, 25);
137    let mut size = Spinner::default().with_size(60, 25);
138
139    let mut color = Choice::default().with_size(100, 25);
140    let mut btn_clear = Button::default().with_size(40, 25).with_label("X");
141    hpack.end();
142
143    vpack.end();
144    wind.end();
145    wind.show();
146
147    text_editor.wrap_mode(fltk::text::WrapMode::AtBounds, 0);
148    text_editor.set_buffer(TextBuffer::default());
149
150    font.add_choice("Courier|Helvetica|Times");
151    font.set_value(0);
152    font.set_tooltip("Font");
153
154    choice.add_choice("Normal|Underline|Strike");
155    choice.set_value(0);
156
157    size.set_value(18.0);
158    size.set_step(1.0);
159    size.set_range(12.0, 28.0);
160    size.set_tooltip("Size");
161
162    color.set_tooltip("Color");
163    color.add_choice("#000000|#ff0000|#00ff00|#0000ff|#ffff00|#00ffff");
164    color.set_value(0);
165
166    btn_clear.set_label_color(Color::Red);
167    btn_clear.set_tooltip("Clear style");
168
169    // set colors
170    for mut item in color.clone() {
171        if let Some(lbl) = item.label() {
172            item.set_label_color(Color::from_u32(
173                u32::from_str_radix(lbl.trim().strip_prefix('#').unwrap(), 16)
174                    .ok()
175                    .unwrap(),
176            ));
177        }
178    }
179
180    let style_rc1 = Rc::clone(&style);
181
182    text_editor.buffer().unwrap().add_modify_callback({
183        let mut text_editor1 = text_editor.clone();
184        let font1 = font.clone();
185        let size1 = size.clone();
186        let color1 = color.clone();
187        let choice1 = choice.clone();
188        move |_buf, pos: i32, ins_items: i32, del_items: i32, _: i32, _: Option<&str>| {
189            let attr = if choice1.value() == 1 {
190                TextAttr::Underline
191            } else if choice1.value() == 2 {
192                TextAttr::StrikeThrough
193            } else {
194                TextAttr::None
195            };
196            if ins_items > 0 || del_items > 0 {
197                let mut style = style_rc1.borrow_mut();
198                let color = Color::from_u32(
199                    u32::from_str_radix(
200                        color1
201                            .text(color1.value())
202                            .unwrap()
203                            .trim()
204                            .strip_prefix('#')
205                            .unwrap(),
206                        16,
207                    )
208                    .ok()
209                    .unwrap(),
210                );
211                style.apply_style(
212                    Some(pos),
213                    Some(ins_items),
214                    Some(del_items),
215                    None,
216                    None,
217                    Font::by_name(font1.text(font1.value()).unwrap().trim()),
218                    size1.value() as i32,
219                    color,
220                    attr,
221                    &mut text_editor1,
222                );
223            }
224        }
225    });
226
227    color.set_callback({
228        let size = size.clone();
229        let font = font.clone();
230        let choice = choice.clone();
231        let mut text_editor = text_editor.clone();
232        let style_rc1 = Rc::clone(&style);
233        move |color| {
234            let attr = match choice.value() {
235                0 => TextAttr::None,
236                1 => TextAttr::Underline,
237                2 => TextAttr::StrikeThrough,
238                _ => unreachable!(),
239            };
240            if let Some(buf) = text_editor.buffer() {
241                if let Some((s, e)) = buf.selection_position() {
242                    let mut style = style_rc1.borrow_mut();
243                    let color = Color::from_u32(
244                        u32::from_str_radix(
245                            color
246                                .text(color.value())
247                                .unwrap()
248                                .trim()
249                                .strip_prefix('#')
250                                .unwrap(),
251                            16,
252                        )
253                        .ok()
254                        .unwrap(),
255                    );
256                    style.apply_style(
257                        None,
258                        None,
259                        None,
260                        Some(s),
261                        Some(e),
262                        Font::by_name(font.text(font.value()).unwrap().trim()),
263                        size.value() as i32,
264                        color,
265                        attr,
266                        &mut text_editor,
267                    );
268                }
269            }
270        }
271    });
272
273    // get the style from the current cursor position
274    text_editor.handle({
275        let style_rc1 = Rc::clone(&style);
276        let mut font1 = font.clone();
277        let mut size1 = size.clone();
278        let mut color1 = color.clone();
279        move |te, e| match e {
280            Event::KeyUp | Event::Released => {
281                if let Some(buff) = te.style_buffer() {
282                    let i = te.insert_position();
283                    if let Some(t) = buff.text_range(i, i + 1) {
284                        if !t.is_empty() {
285                            let style = style_rc1.borrow_mut();
286                            if let Some(i) = t.chars().next().map(|c| (c as usize - 65)) {
287                                if let Some(style) = style.style_table.get(i) {
288                                    if let Some(mn) = font1.find_item(&format!("{:?}", style.font))
289                                    {
290                                        font1.set_item(&mn);
291                                    }
292                                    size1.set_value(style.size as f64);
293                                    let (r, g, b) = style.color.to_rgb();
294                                    if let Some(mn) =
295                                        color1.find_item(format!("{r:02x}{g:02x}{b:02x}").as_str())
296                                    {
297                                        color1.set_item(&mn);
298                                    }
299                                }
300                            }
301                        }
302                    }
303                }
304                true
305            }
306            _ => false,
307        }
308    });
309
310    choice.set_callback({
311        let mut color1 = color.clone();
312        move |_| color1.do_callback()
313    });
314
315    font.set_callback({
316        let mut color1 = color.clone();
317        move |_| color1.do_callback()
318    });
319
320    size.set_callback({
321        let mut color1 = color.clone();
322        move |_| color1.do_callback()
323    });
324
325    // clear style of the current selection or, if no text is selected, clear all text style
326    btn_clear.set_callback({
327        let style_rc1 = Rc::clone(&style);
328        let text_editor1 = text_editor.clone();
329        move |_| {
330            match text_editor1.buffer().unwrap().selection_position() {
331                Some((_, _)) => {
332                    font.set_value(0);
333                    size.set_value(18.0);
334                    color.set_value(0);
335                    choice.set_value(0);
336                    color.do_callback();
337                }
338                None => {
339                    font.set_value(0);
340                    size.set_value(18.0);
341                    color.set_value(0);
342                    style_rc1.borrow_mut().apply_style(
343                        None,
344                        None,
345                        None,
346                        Some(0),
347                        Some(text_editor1.buffer().unwrap().length()),
348                        Font::Courier,
349                        16,
350                        Color::Black,
351                        TextAttr::None,
352                        &mut text_editor,
353                    );
354                }
355            };
356        }
357    });
358
359    app.run().unwrap();
360}