use poll_promise::Promise; struct Resource { /// HTTP response response: ehttp::Response, text: Option, /// If set, the response was an image. texture: Option, /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). colored_text: Option, } impl Resource { fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self { let content_type = response.content_type().unwrap_or_default(); let image = if content_type.starts_with("image/") { load_image(&response.bytes).ok() } else { None }; let texture = image.map(|image| ctx.load_texture(&response.url, image)); let text = response.text(); let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); let text = text.map(|text| text.to_owned()); Self { response, text, texture, colored_text, } } } #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct HttpApp { url: String, #[cfg_attr(feature = "serde", serde(skip))] promise: Option>>, } impl Default for HttpApp { fn default() -> Self { Self { url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(), promise: Default::default(), } } } impl epi::App for HttpApp { fn name(&self) -> &str { "⬇ HTTP" } fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { egui::TopBottomPanel::bottom("http_bottom").show(ctx, |ui| { let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true); ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| { ui.add(crate::__egui_github_link_file!()) }) }); egui::CentralPanel::default().show(ctx, |ui| { let trigger_fetch = ui_url(ui, frame, &mut self.url); ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("HTTP requests made using "); ui.hyperlink_to("ehttp", "https://www.github.com/emilk/ehttp"); ui.label("."); }); if trigger_fetch { let ctx = ctx.clone(); let frame = frame.clone(); let (sender, promise) = Promise::new(); let request = ehttp::Request::get(&self.url); ehttp::fetch(request, move |response| { frame.request_repaint(); // wake up UI thread let resource = response.map(|response| Resource::from_response(&ctx, response)); sender.send(resource); }); self.promise = Some(promise); } ui.separator(); if let Some(promise) = &self.promise { if let Some(result) = promise.ready() { match result { Ok(resource) => { ui_resource(ui, resource); } Err(error) => { // This should only happen if the fetch API isn't available or something similar. ui.colored_label( egui::Color32::RED, if error.is_empty() { "Error" } else { error }, ); } } } else { ui.add(egui::Spinner::new()); } } }); } } fn ui_url(ui: &mut egui::Ui, frame: &epi::Frame, url: &mut String) -> bool { let mut trigger_fetch = false; ui.horizontal(|ui| { ui.label("URL:"); trigger_fetch |= ui .add(egui::TextEdit::singleline(url).desired_width(f32::INFINITY)) .lost_focus(); }); if frame.is_web() { ui.label("HINT: paste the url of this page into the field above!"); } ui.horizontal(|ui| { if ui.button("Source code for this example").clicked() { *url = format!( "https://raw.githubusercontent.com/emilk/egui/master/{}", file!() ); trigger_fetch = true; } if ui.button("Random image").clicked() { let seed = ui.input().time; let side = 640; *url = format!("https://picsum.photos/seed/{}/{}", seed, side); trigger_fetch = true; } }); trigger_fetch } fn ui_resource(ui: &mut egui::Ui, resource: &Resource) { let Resource { response, text, texture, colored_text, } = resource; ui.monospace(format!("url: {}", response.url)); ui.monospace(format!( "status: {} ({})", response.status, response.status_text )); ui.monospace(format!( "content-type: {}", response.content_type().unwrap_or_default() )); ui.monospace(format!( "size: {:.1} kB", response.bytes.len() as f32 / 1000.0 )); ui.separator(); egui::ScrollArea::vertical() .auto_shrink([false; 2]) .show(ui, |ui| { egui::CollapsingHeader::new("Response headers") .default_open(false) .show(ui, |ui| { egui::Grid::new("response_headers") .spacing(egui::vec2(ui.spacing().item_spacing.x * 2.0, 0.0)) .show(ui, |ui| { for header in &response.headers { ui.label(header.0); ui.label(header.1); ui.end_row(); } }) }); ui.separator(); if let Some(text) = &text { let tooltip = "Click to copy the response body"; if ui.button("📋").on_hover_text(tooltip).clicked() { ui.output().copied_text = text.clone(); } ui.separator(); } if let Some(texture) = texture { let mut size = texture.size_vec2(); size *= (ui.available_width() / size.x).min(1.0); ui.image(texture, size); } else if let Some(colored_text) = colored_text { colored_text.ui(ui); } else if let Some(text) = &text { selectable_text(ui, text); } else { ui.monospace("[binary]"); } }); } fn selectable_text(ui: &mut egui::Ui, mut text: &str) { ui.add( egui::TextEdit::multiline(&mut text) .desired_width(f32::INFINITY) .font(egui::TextStyle::Monospace), ); } // ---------------------------------------------------------------------------- // Syntax highlighting: #[cfg(feature = "syntect")] fn syntax_highlighting( ctx: &egui::Context, response: &ehttp::Response, text: &str, ) -> Option { let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect(); let extension = extension_and_rest.get(0)?; let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style()); Some(ColoredText(crate::syntax_highlighting::highlight( ctx, &theme, text, extension, ))) } #[cfg(not(feature = "syntect"))] fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option { None } struct ColoredText(egui::text::LayoutJob); impl ColoredText { pub fn ui(&self, ui: &mut egui::Ui) { if true { // Selectable text: let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| { let mut layout_job = self.0.clone(); layout_job.wrap_width = wrap_width; ui.fonts().layout_job(layout_job) }; let mut text = self.0.text.as_str(); ui.add( egui::TextEdit::multiline(&mut text) .font(egui::TextStyle::Monospace) .desired_width(f32::INFINITY) .layouter(&mut layouter), ); } else { let mut job = self.0.clone(); job.wrap_width = ui.available_width(); let galley = ui.fonts().layout_job(job); let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover()); painter.add(egui::Shape::galley(response.rect.min, galley)); } } } // ---------------------------------------------------------------------------- fn load_image(image_data: &[u8]) -> Result { let image = image::load_from_memory(image_data)?; let size = [image.width() as _, image.height() as _]; let image_buffer = image.to_rgba8(); let pixels = image_buffer.as_flat_samples(); Ok(egui::ColorImage::from_rgba_unmultiplied( size, pixels.as_slice(), )) }