I’m developing an editor for a typesetting language (like LaTeX) with Iced. The screen consists of a toolbar, a text editing area and a preview area. When the “preview” button (or Ctrl + S) is pressed, the text is compiled into the typesetting language, which returns a list of SVGs. In the view()
method, SVG widgets are then created.
pub struct Editing {
tool_bar: ToolBar,
current_buffer: Buffer,
buffers: HashMap<FileId, Buffer>,
current_dir: PathBuf,
preview: Preview,
typst: TideWorld,
auto_pairs: HashMap<char, char>,
split_at: f32,
}
impl Editing {
pub fn new(config: EditorConfig, current_dir: PathBuf) -> Self {
Self {
tool_bar: ToolBar::new(true),
current_buffer: Buffer::new(),
buffers: HashMap::new(),
current_dir: current_dir.clone(),
preview: Preview::new(),
typst: init_world(),
auto_pairs: config.auto_pairs,
split_at: 800.0,
}
}
fn add_buffer(&mut self, file_id: FileId, source: Source) {
let buffer = Buffer::from_content(Content::with_text(source.text()));
self.buffers.insert(
file_id,
buffer
);
}
pub fn view(&self) -> Element<Message> {
let tool_bar = self.tool_bar.view().map(Message::ToolBar);
let editor = TextEditor::new(&self.current_buffer.content)
.on_action(Message::ActionPerformed)
.placeholder("Insert text here or open a new file")
.key_binding(|key_press| {
if let Some(ref text) = key_press.text {
if let Some(actual_char) = text.chars().nth(0) {
if self.auto_pairs.contains_key(&actual_char) {
if let Some(selection) = self.current_buffer.content.selection() {
let mut seq: Vec<Binding<Message>> = Vec::new();
seq.push(Binding::Insert(actual_char));
for c in selection.chars() {
seq.push(Binding::Insert(c));
}
seq.push(Binding::Insert(
*self.auto_pairs.get(&actual_char).unwrap(),
));
Some(Binding::Sequence(seq))
} else {
Some(Binding::Sequence(vec![
Binding::Insert(actual_char),
Binding::Insert(*self.auto_pairs.get(&actual_char).unwrap()),
Binding::Move(Motion::Left),
]))
}
} else {
bindings(key_press)
}
} else {
bindings(key_press)
}
} else {
bindings(key_press)
}
})
.wrapping(iced::widget::text::Wrapping::WordOrGlyph)
.height(Fill);
if let Some(preview_pages) = &self.preview.content {
let mut svg_pages = vec![];
for page in preview_pages {
svg_pages.push(svg(Handle::from_memory(page.clone().into_bytes())).into());
}
let preview = Scrollable::new(Column::with_children(svg_pages).spacing(15).padding(15))
.width(Fill)
.height(Fill);
let main_screen = VSplit::new(editor, preview)
.strategy(crate::widget::vsplit::Strategy::Left)
.split_at(self.split_at)
.on_resize(Message::ResizePreview);
column![tool_bar, main_screen].into()
} else {
column![tool_bar, editor].into()
}
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ResizePreview(split_at) => {
self.split_at = split_at.clamp(200.0, 1500.0);
Task::none()
}
Message::ActionPerformed(action) => {
self.current_buffer.content.perform(action);
Task::none()
}
Message::SvgGenerated(result) => {
println!("async: SVG generated");
match result {
Ok(svg) => self.preview.content = Some(svg),
Err(err) => println!("Error while generating SVG: {:?}", err),
}
Task::none()
}
Message::ToolBar(message) => {
match message {
toolbar::Message::ForcePreview => {
self.typst
.reload_from_content(self.current_buffer.content.text().as_str());
Task::perform(
preview_svg(self.typst.clone()),
|result| Message::SvgGenerated(result))
},
toolbar::Message::FileImported(result) => {
match result {
Ok(path) => {
println!("file imported: {:?}", path);
},
Err(error) => {
println!("import error: {:?}", error);
}
}
Task::none()
}
toolbar::Message::OpenFile => {
let path = load_file_dialog("*", &["png", "jpg", "jpeg", "typ", "svg"]);
if let Some(import_path) = path {
let result = load_file(&import_path);
if let Ok(imported_file) = result {
match imported_file {
ImportedFile::Asset { file_id, bytes } => {
println!("imported asset: {:?}", file_id);
self.typst.add_asset(file_id, bytes);
}
ImportedFile::TypstSource { file_id, source } => {
println!("imported source file: {:?}", file_id);
self.add_buffer(file_id, source.clone());
if let Some(buffer) = self.buffers.get(&file_id) {
self.current_buffer = buffer.clone();
}
self.typst.add_source(file_id, source);
}
}
}
Task::done(Message::ToolBar(toolbar::Message::FileImported(Ok(import_path))))
} else {
Task::none()
}
}
toolbar::Message::Export(export_type) => match export_type {
ExportType::PDF => {
self.typst
.reload_from_content(self.current_buffer.content.text().as_str());
let path = save_file_dialog("pdf", &["pdf"]);
if let Some(export_path) = path {
Task::perform(export_pdf(self.typst.clone(), export_path, PdfOptions::default()),
|result| Message::ToolBar(toolbar::Message::ProjectExported(result)))
} else {
Task::none()
}
}
ExportType::SVG => {
self.typst
.reload_from_content(self.current_buffer.content.text().as_str());
let path = save_file_dialog("svg", &["svg"]);
if let Some(export_path) = path {
Task::perform(export_svg(self.typst.clone(), export_path),
|result| Message::ToolBar(toolbar::Message::ProjectExported(result)))
} else {
Task::none()
}
}
ExportType::Template => Task::none(),
}
toolbar::Message::ProjectExported(result) => {
match result {
Ok(path) => { println!("project exported at {:?}", path); }
Err(error) => { println!("project not exported: {:?}", error); }
}
Task::none()
},
_ => Task::none(),
}
}
_ => Task::none(),
}
}
}
The problem with this (synchronous) method is that if the SVGs are complex, the application freezes or even crashes (indicating that it has run out of memory). I’ve managed to make compilation (and export) asynchronous using Task::perform()
. But I can’t get the generation of SVG widgets not to block the UI if their creation takes several seconds.
Do you have any idea how to do this?
I think it’s this part of the code that blocks the UI if there are too many complex SVGs:
let mut svg_pages = vec![];
for page in preview_pages { svg_pages.push(svg(Handle::from_memory(page.clone().into_bytes())).into());
}
let preview = Scrollable::new(Column::with_children(svg_pages).spacing(15).padding(15))
.width(Fill)
.height(Fill);
Thank you in advance for your help.