Skip to content

Commit acde7e9

Browse files
authored
support popup preview from plugin (#93)
1 parent f865eba commit acde7e9

File tree

19 files changed

+739
-489
lines changed

19 files changed

+739
-489
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/kiorg/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,3 @@ windows-sys = { version = "0.61.0", features = [
134134
name = "Kiorg"
135135
icon = ["../../assets/icons/1024x1024@2x.png"]
136136
osx_url_schemes = ["com.kiorg.kiorg"]
137-

crates/kiorg/src/app.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,30 @@ impl Kiorg {
369369
notification::check_notifications(self);
370370
}
371371

372+
/// Check and process background preview loading
373+
pub fn new_preview_loaded(&mut self, ctx: &egui::Context) -> bool {
374+
let receiver = match &self.preview_content {
375+
Some(PreviewContent::Loading(_path, receiver, _cancel_sender)) => receiver.clone(),
376+
_ => return false,
377+
};
378+
if let Ok(receiver_lock) = receiver.lock() {
379+
if let Ok(result) = receiver_lock.try_recv() {
380+
ctx.request_repaint();
381+
match result {
382+
Ok(content) => {
383+
self.preview_content = Some(content);
384+
}
385+
Err(e) => {
386+
self.preview_content =
387+
Some(PreviewContent::text(format!("Error loading file: {e}")));
388+
}
389+
}
390+
return true;
391+
}
392+
}
393+
false
394+
}
395+
372396
/// Get shortcuts from config or use defaults
373397
/// This method provides a centralized way to access shortcuts configuration
374398
/// that can be reused across the main input handler and popup components
@@ -925,6 +949,11 @@ impl eframe::App for Kiorg {
925949
#[cfg(feature = "debug")]
926950
ctx.set_debug_on_hover(true);
927951

952+
if self.new_preview_loaded(ctx) {
953+
return;
954+
}
955+
self.check_notifications();
956+
928957
if self
929958
.notify_fs_change
930959
.load(std::sync::atomic::Ordering::Relaxed)
@@ -941,9 +970,6 @@ impl eframe::App for Kiorg {
941970
.store(false, std::sync::atomic::Ordering::Relaxed);
942971
}
943972

944-
// Check for notification messages from background threads
945-
self.check_notifications();
946-
947973
// Update preview cache only if selection changed
948974
if self.selection_changed {
949975
preview::update_cache(self, ctx);
@@ -1024,17 +1050,14 @@ impl eframe::App for Kiorg {
10241050
#[cfg(target_os = "macos")]
10251051
Some(PopupType::Volumes(_)) => {
10261052
use crate::ui::popup::volumes;
1027-
1028-
// Handle volumes popup
10291053
let volume_action = volumes::show_volumes_popup(ctx, self);
1030-
// Process the volume action
10311054
match volume_action {
10321055
volumes::VolumeAction::Navigate(path) => self.navigate_to_dir(path),
10331056
volumes::VolumeAction::None => {}
10341057
};
10351058
}
10361059
Some(PopupType::Preview) => {
1037-
popup_preview::show_preview_popup(ctx, self);
1060+
popup_preview::draw(ctx, self);
10381061
}
10391062
Some(PopupType::Themes(_)) => {
10401063
theme::draw(self, ctx);

crates/kiorg/src/input.rs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -307,22 +307,14 @@ fn process_key(
307307
match &app.show_popup {
308308
Some(PopupType::Preview) => {
309309
if is_cancel_keys(key) {
310-
app.show_popup = None;
310+
popup_preview::close_popup(app);
311311
return;
312312
}
313-
314313
// Handle preview popup input (PDF page navigation, etc.)
315-
match &mut app.preview_content {
316-
Some(crate::models::preview_content::PreviewContent::Pdf(pdf_meta)) => {
317-
popup_preview::doc::handle_preview_popup_input_pdf(
318-
pdf_meta, key, modifiers, ctx,
319-
);
320-
}
321-
Some(crate::models::preview_content::PreviewContent::Epub(_epub_meta)) => {
322-
// EPUB documents don't have page navigation in preview popup
323-
// Only handle ESC to close popup which is already handled above
324-
}
325-
_ => {}
314+
if let Some(crate::models::preview_content::PreviewContent::Pdf(pdf_meta)) =
315+
&mut app.preview_content
316+
{
317+
popup_preview::doc::handle_preview_popup_input_pdf(pdf_meta, key, modifiers, ctx);
326318
}
327319
return;
328320
}

crates/kiorg/src/models/preview_content.rs

Lines changed: 131 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ pub struct ImageMeta {
9797
pub metadata: HashMap<String, String>,
9898
/// EXIF metadata (key-value pairs), stored separately from regular metadata
9999
pub exif_data: Option<HashMap<String, String>>,
100-
/// Image source (can be texture handle or URI for animated images)
101-
pub image_source: egui::widgets::ImageSource<'static>,
100+
/// Pre-constructed image widget ready for rendering
101+
pub image: egui::Image<'static>,
102102
/// Keep the texture handle alive to prevent GPU texture from being freed
103103
pub _texture_handle: Option<egui::TextureHandle>,
104104
}
@@ -110,7 +110,7 @@ impl std::fmt::Debug for ImageMeta {
110110
.field("title", &self.title)
111111
.field("metadata", &self.metadata)
112112
.field("exif_data", &self.exif_data)
113-
.field("image_source", &"ImageSource")
113+
.field("image", &"Image")
114114
.field(
115115
"_texture_handle",
116116
&self._texture_handle.as_ref().map(|_| "TextureHandle"),
@@ -130,9 +130,7 @@ pub enum PreviewContent {
130130
language: &'static str,
131131
},
132132
/// Plugin-generated preview content
133-
PluginPreview {
134-
components: Vec<kiorg_plugin::Component>,
135-
},
133+
PluginPreview { components: Vec<RenderedComponent> },
136134
/// Image content with metadata
137135
Image(ImageMeta),
138136
/// Zip file content with a list of entries
@@ -182,15 +180,134 @@ pub struct TarEntry {
182180
pub permissions: String,
183181
}
184182

183+
fn load_into_texture(
184+
ctx: &egui::Context,
185+
dynamic_image: image::DynamicImage,
186+
name: String,
187+
) -> (egui::Image<'static>, Option<egui::TextureHandle>) {
188+
let rgba8 = dynamic_image.to_rgba8();
189+
let size = [rgba8.width() as usize, rgba8.height() as usize];
190+
let color_image =
191+
egui::ColorImage::from_rgba_unmultiplied(size, rgba8.as_flat_samples().as_slice());
192+
let texture = ctx.load_texture(name, color_image, Default::default());
193+
let image = egui::Image::new(&texture);
194+
(image, Some(texture))
195+
}
196+
197+
/// Rendered version of plugin components that can hold processed data like textures
198+
#[derive(Clone, Debug)]
199+
pub enum RenderedComponent {
200+
Title(kiorg_plugin::TitleComponent),
201+
Text(kiorg_plugin::TextComponent),
202+
Image(RenderedImageComponent),
203+
Table(kiorg_plugin::TableComponent),
204+
}
205+
206+
#[derive(Clone)]
207+
pub struct RenderedImageComponent {
208+
pub image: egui::Image<'static>,
209+
pub interactive: bool,
210+
pub _texture_handle: Option<egui::TextureHandle>,
211+
}
212+
213+
impl std::fmt::Debug for RenderedImageComponent {
214+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215+
f.debug_struct("RenderedImageComponent")
216+
.field("image", &"Image")
217+
.field("interactive", &self.interactive)
218+
.field(
219+
"_texture_handle",
220+
&self._texture_handle.as_ref().map(|_| "TextureHandle"),
221+
)
222+
.finish()
223+
}
224+
}
225+
185226
impl PreviewContent {
186227
/// Creates a new text preview content
187228
pub fn text(content: impl Into<String>) -> Self {
188229
Self::Text(content.into())
189230
}
190231

191-
/// Creates a new plugin preview content
192-
pub fn plugin_preview(components: Vec<kiorg_plugin::Component>) -> Self {
193-
Self::PluginPreview { components }
232+
/// Creates a new plugin preview content by processing plugin components
233+
pub fn plugin_preview_from_components(
234+
components: Vec<kiorg_plugin::Component>,
235+
ctx: &egui::Context,
236+
) -> Self {
237+
let mut rendered_components = Vec::with_capacity(components.len());
238+
239+
for component in components {
240+
match component {
241+
kiorg_plugin::Component::Title(t) => {
242+
rendered_components.push(RenderedComponent::Title(t))
243+
}
244+
kiorg_plugin::Component::Text(t) => {
245+
rendered_components.push(RenderedComponent::Text(t))
246+
}
247+
kiorg_plugin::Component::Table(t) => {
248+
rendered_components.push(RenderedComponent::Table(t))
249+
}
250+
kiorg_plugin::Component::Image(img) => match img.source {
251+
kiorg_plugin::ImageSource::Path(path) => match image::open(&path) {
252+
Ok(dynamic_image) => {
253+
let (image, texture_handle) = load_into_texture(
254+
ctx,
255+
dynamic_image,
256+
format!("plugin_preview_path_{}", path),
257+
);
258+
rendered_components.push(RenderedComponent::Image(
259+
RenderedImageComponent {
260+
image,
261+
interactive: img.interactive,
262+
_texture_handle: texture_handle,
263+
},
264+
));
265+
}
266+
Err(e) => {
267+
rendered_components.push(RenderedComponent::Text(
268+
kiorg_plugin::TextComponent {
269+
text: format!(
270+
"Failed to load image from path: {}\nError: {}",
271+
path, e
272+
),
273+
},
274+
));
275+
}
276+
},
277+
kiorg_plugin::ImageSource::Bytes { format, data, uid } => {
278+
match image::load_from_memory_with_format(&data, format) {
279+
Ok(dynamic_image) => {
280+
let (image, texture_handle) = load_into_texture(
281+
ctx,
282+
dynamic_image,
283+
format!("plugin_preview_bytes_{}", uid),
284+
);
285+
rendered_components.push(RenderedComponent::Image(
286+
RenderedImageComponent {
287+
image,
288+
interactive: img.interactive,
289+
_texture_handle: texture_handle,
290+
},
291+
));
292+
}
293+
Err(e) => {
294+
rendered_components.push(RenderedComponent::Text(
295+
kiorg_plugin::TextComponent {
296+
text: format!(
297+
"Failed to decode image (format: {:?}, uid: {}\nError: {}",
298+
format, uid, e
299+
),
300+
},
301+
));
302+
}
303+
}
304+
}
305+
},
306+
}
307+
}
308+
Self::PluginPreview {
309+
components: rendered_components,
310+
}
194311
}
195312

196313
/// Creates a new image preview content with a texture handle
@@ -200,11 +317,12 @@ impl PreviewContent {
200317
texture: egui::TextureHandle,
201318
exif_data: Option<HashMap<String, String>>,
202319
) -> Self {
320+
let image = egui::Image::new(&texture);
203321
Self::Image(ImageMeta {
204322
title: title.into(),
205323
metadata,
206324
exif_data,
207-
image_source: egui::widgets::ImageSource::from(&texture),
325+
image,
208326
_texture_handle: Some(texture),
209327
})
210328
}
@@ -216,12 +334,13 @@ impl PreviewContent {
216334
uri: String,
217335
exif_data: Option<HashMap<String, String>>,
218336
) -> Self {
337+
let image = egui::Image::new(egui::widgets::ImageSource::Uri(uri.into()));
219338
Self::Image(ImageMeta {
220339
title: title.into(),
221340
metadata,
222341
exif_data,
223-
image_source: egui::widgets::ImageSource::Uri(uri.into()),
224-
_texture_handle: None, // No texture handle for URI-based images//
342+
image,
343+
_texture_handle: None, // No texture handle for URI-based images
225344
})
226345
}
227346

crates/kiorg/src/plugins/manager.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,32 @@ impl Drop for LoadedPlugin {
8282

8383
impl LoadedPlugin {
8484
/// Execute preview command on the plugin for the given file path
85-
pub fn preview_file(
85+
pub fn preview(&self, file_path: &str) -> Result<Vec<kiorg_plugin::Component>, PluginError> {
86+
self.call_preview_internal(
87+
EngineCommand::Preview {
88+
path: file_path.to_string(),
89+
},
90+
file_path,
91+
)
92+
}
93+
94+
/// Execute preview popup command on the plugin for the given file path
95+
pub fn preview_popup(
8696
&self,
8797
file_path: &str,
98+
) -> Result<Vec<kiorg_plugin::Component>, PluginError> {
99+
self.call_preview_internal(
100+
EngineCommand::PreviewPopup {
101+
path: file_path.to_string(),
102+
},
103+
file_path,
104+
)
105+
}
106+
107+
fn call_preview_internal(
108+
&self,
109+
command: EngineCommand,
110+
file_path: &str,
88111
) -> Result<Vec<kiorg_plugin::Component>, PluginError> {
89112
let mut state = self.state.lock().expect("Failed to lock plugin state");
90113

@@ -97,15 +120,13 @@ impl LoadedPlugin {
97120
// Create the preview command message
98121
let engine_message = EngineMessage {
99122
id: CallId::new(),
100-
command: EngineCommand::Preview {
101-
path: file_path.to_string(),
102-
},
123+
command,
103124
};
104125

105126
let plugin_name = &self.metadata.name;
106127
debug!(
107-
"Sending preview message to plugin '{}': {:?}",
108-
plugin_name, engine_message
128+
"Sending preview message to plugin '{}' for '{}': {:?}",
129+
plugin_name, file_path, engine_message
109130
);
110131

111132
// Send the message to plugin stdin with length prefix
@@ -119,6 +140,9 @@ impl LoadedPlugin {
119140
// Extract the preview content
120141
match plugin_response {
121142
kiorg_plugin::PluginResponse::Preview { components } => Ok(components),
143+
kiorg_plugin::PluginResponse::Error { message } => {
144+
Err(PluginError::ExecutionError { message })
145+
}
122146
_ => Err(PluginError::ProtocolError {
123147
message: "Expected Preview response from plugin".to_string(),
124148
}),
@@ -427,6 +451,9 @@ impl PluginManager {
427451
protocol_version,
428452
metadata: Box::new(metadata),
429453
}),
454+
kiorg_plugin::PluginResponse::Error { message } => {
455+
Err(PluginError::ExecutionError { message })
456+
}
430457
_ => Err(PluginError::ProtocolError {
431458
message: "Expected Hello response from plugin".to_string(),
432459
}),

0 commit comments

Comments
 (0)