2021-08-15 14:56:46 +00:00
|
|
|
use epi::http::{Request, Response};
|
2020-11-17 23:43:58 +00:00
|
|
|
use std::sync::mpsc::Receiver;
|
|
|
|
|
2020-11-18 20:38:29 +00:00
|
|
|
struct Resource {
|
|
|
|
/// HTTP response
|
|
|
|
response: Response,
|
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
text: Option<String>,
|
|
|
|
|
2020-11-18 20:38:29 +00:00
|
|
|
/// If set, the response was an image.
|
|
|
|
image: Option<Image>,
|
2020-12-28 18:50:48 +00:00
|
|
|
|
|
|
|
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
|
|
|
|
colored_text: Option<ColoredText>,
|
2020-11-18 20:38:29 +00:00
|
|
|
}
|
|
|
|
|
2020-11-20 18:50:47 +00:00
|
|
|
impl Resource {
|
|
|
|
fn from_response(response: Response) -> Self {
|
2021-08-15 14:56:46 +00:00
|
|
|
let content_type = response.content_type().unwrap_or_default();
|
|
|
|
let image = if content_type.starts_with("image/") {
|
2020-11-20 18:50:47 +00:00
|
|
|
Image::decode(&response.bytes)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
2020-12-28 18:50:48 +00:00
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
let text = response.text();
|
|
|
|
|
|
|
|
let colored_text = text
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|text| syntax_highlighting(&response, text));
|
2020-12-28 18:50:48 +00:00
|
|
|
|
|
|
|
Self {
|
|
|
|
response,
|
2021-08-15 14:56:46 +00:00
|
|
|
text,
|
2020-12-28 18:50:48 +00:00
|
|
|
image,
|
|
|
|
colored_text,
|
|
|
|
}
|
2020-11-20 18:50:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
#[derive(Debug, PartialEq, Copy, Clone)]
|
|
|
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
|
|
|
enum Method {
|
|
|
|
Get,
|
|
|
|
Post,
|
|
|
|
}
|
|
|
|
|
2021-01-04 00:44:02 +00:00
|
|
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
2021-01-01 16:11:05 +00:00
|
|
|
pub struct HttpApp {
|
2020-11-17 23:43:58 +00:00
|
|
|
url: String,
|
2021-01-01 16:11:05 +00:00
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
method: Method,
|
|
|
|
|
|
|
|
request_body: String,
|
|
|
|
|
2021-01-04 00:44:02 +00:00
|
|
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
2020-11-18 20:38:29 +00:00
|
|
|
in_progress: Option<Receiver<Result<Response, String>>>,
|
2021-01-01 16:11:05 +00:00
|
|
|
|
2021-01-04 00:44:02 +00:00
|
|
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
2020-11-18 20:38:29 +00:00
|
|
|
result: Option<Result<Resource, String>>,
|
2021-01-01 16:11:05 +00:00
|
|
|
|
2021-01-04 00:44:02 +00:00
|
|
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
2020-11-20 18:50:47 +00:00
|
|
|
tex_mngr: TexMngr,
|
2020-11-17 22:24:14 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 16:11:05 +00:00
|
|
|
impl Default for HttpApp {
|
2020-11-17 22:24:14 +00:00
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
2020-11-17 23:43:58 +00:00
|
|
|
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
|
2021-08-15 14:56:46 +00:00
|
|
|
method: Method::Get,
|
|
|
|
request_body: r#"["posting some json", { "more_json" : true }]"#.to_owned(),
|
2020-11-18 20:38:29 +00:00
|
|
|
in_progress: Default::default(),
|
|
|
|
result: Default::default(),
|
2020-11-20 18:50:47 +00:00
|
|
|
tex_mngr: Default::default(),
|
2020-11-17 22:24:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-01 16:11:05 +00:00
|
|
|
impl epi::App for HttpApp {
|
2020-12-18 21:30:59 +00:00
|
|
|
fn name(&self) -> &str {
|
2021-01-01 23:13:34 +00:00
|
|
|
"⬇ HTTP"
|
2020-12-18 21:30:59 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 22:24:14 +00:00
|
|
|
/// Called each time the UI needs repainting, which may be many times per second.
|
2021-05-26 20:06:10 +00:00
|
|
|
/// Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`.
|
2021-01-01 19:22:18 +00:00
|
|
|
fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
|
2020-11-20 19:35:16 +00:00
|
|
|
if let Some(receiver) = &mut self.in_progress {
|
|
|
|
// Are we there yet?
|
|
|
|
if let Ok(result) = receiver.try_recv() {
|
|
|
|
self.in_progress = None;
|
|
|
|
self.result = Some(result.map(Resource::from_response));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-17 22:24:14 +00:00
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
2021-01-02 00:01:01 +00:00
|
|
|
ui.heading("HTTP Fetch Example");
|
2020-11-17 23:43:58 +00:00
|
|
|
ui.add(egui::github_link_file!(
|
|
|
|
"https://github.com/emilk/egui/blob/master/",
|
|
|
|
"(source code)"
|
|
|
|
));
|
2020-11-17 22:24:14 +00:00
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
if let Some(request) = ui_url(
|
|
|
|
ui,
|
|
|
|
frame,
|
|
|
|
&mut self.url,
|
|
|
|
&mut self.method,
|
|
|
|
&mut self.request_body,
|
|
|
|
) {
|
2020-12-31 13:31:11 +00:00
|
|
|
let repaint_signal = frame.repaint_signal();
|
2020-11-18 20:38:29 +00:00
|
|
|
let (sender, receiver) = std::sync::mpsc::channel();
|
|
|
|
self.in_progress = Some(receiver);
|
2020-12-30 19:56:50 +00:00
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
frame.http_fetch(request, move |response| {
|
2020-12-30 19:56:50 +00:00
|
|
|
sender.send(response).ok();
|
2020-11-20 19:35:16 +00:00
|
|
|
repaint_signal.request_repaint();
|
2020-11-17 23:43:58 +00:00
|
|
|
});
|
|
|
|
}
|
2020-11-17 22:24:14 +00:00
|
|
|
|
2020-11-18 20:38:29 +00:00
|
|
|
ui.separator();
|
|
|
|
|
|
|
|
if self.in_progress.is_some() {
|
|
|
|
ui.label("Please wait...");
|
|
|
|
} else if let Some(result) = &self.result {
|
2020-11-17 23:43:58 +00:00
|
|
|
match result {
|
2020-11-18 20:38:29 +00:00
|
|
|
Ok(resource) => {
|
2021-02-03 19:04:57 +00:00
|
|
|
ui_resource(ui, frame, &mut self.tex_mngr, resource);
|
2020-11-17 23:43:58 +00:00
|
|
|
}
|
|
|
|
Err(error) => {
|
|
|
|
// This should only happen if the fetch API isn't available or something similar.
|
2021-08-15 14:56:46 +00:00
|
|
|
ui.add(
|
|
|
|
egui::Label::new(if error.is_empty() { "Error" } else { error })
|
|
|
|
.text_color(egui::Color32::RED),
|
|
|
|
);
|
2020-11-17 23:43:58 +00:00
|
|
|
}
|
|
|
|
}
|
2020-11-17 22:24:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2020-11-17 23:43:58 +00:00
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
fn ui_url(
|
|
|
|
ui: &mut egui::Ui,
|
|
|
|
frame: &mut epi::Frame<'_>,
|
|
|
|
url: &mut String,
|
|
|
|
method: &mut Method,
|
|
|
|
request_body: &mut String,
|
|
|
|
) -> Option<Request> {
|
2020-11-18 20:38:29 +00:00
|
|
|
let mut trigger_fetch = false;
|
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
egui::Grid::new("request_params").show(ui, |ui| {
|
2020-11-18 20:38:29 +00:00
|
|
|
ui.label("URL:");
|
2021-08-15 14:56:46 +00:00
|
|
|
ui.horizontal(|ui| {
|
|
|
|
trigger_fetch |= ui.text_edit_singleline(url).lost_focus();
|
|
|
|
egui::ComboBox::from_id_source("method")
|
|
|
|
.selected_text(format!("{:?}", method))
|
|
|
|
.width(60.0)
|
|
|
|
.show_ui(ui, |ui| {
|
|
|
|
ui.selectable_value(method, Method::Get, "GET");
|
|
|
|
ui.selectable_value(method, Method::Post, "POST");
|
|
|
|
});
|
|
|
|
trigger_fetch |= ui.button("▶").clicked();
|
|
|
|
});
|
|
|
|
ui.end_row();
|
|
|
|
if *method == Method::Post {
|
|
|
|
ui.label("Body:");
|
|
|
|
ui.add(
|
|
|
|
egui::TextEdit::multiline(request_body)
|
|
|
|
.code_editor()
|
|
|
|
.desired_rows(1),
|
|
|
|
);
|
|
|
|
ui.end_row();
|
|
|
|
}
|
2020-11-20 18:50:47 +00:00
|
|
|
});
|
2020-11-18 20:38:29 +00:00
|
|
|
|
2021-01-02 00:01:01 +00:00
|
|
|
if frame.is_web() {
|
|
|
|
ui.label("HINT: paste the url of this page into the field above!");
|
|
|
|
}
|
2020-12-28 18:50:48 +00:00
|
|
|
|
2020-11-20 18:50:47 +00:00
|
|
|
ui.horizontal(|ui| {
|
2021-01-25 17:50:19 +00:00
|
|
|
if ui.button("Source code for this example").clicked() {
|
2020-11-18 20:38:29 +00:00
|
|
|
*url = format!(
|
|
|
|
"https://raw.githubusercontent.com/emilk/egui/master/{}",
|
|
|
|
file!()
|
|
|
|
);
|
2021-08-15 14:56:46 +00:00
|
|
|
*method = Method::Get;
|
2020-11-18 20:38:29 +00:00
|
|
|
trigger_fetch = true;
|
|
|
|
}
|
2021-01-25 17:50:19 +00:00
|
|
|
if ui.button("Random image").clicked() {
|
2020-11-18 20:38:29 +00:00
|
|
|
let seed = ui.input().time;
|
|
|
|
let width = 640;
|
|
|
|
let height = 480;
|
|
|
|
*url = format!("https://picsum.photos/seed/{}/{}/{}", seed, width, height);
|
2021-08-15 14:56:46 +00:00
|
|
|
*method = Method::Get;
|
|
|
|
trigger_fetch = true;
|
|
|
|
}
|
|
|
|
if ui.button("Post to httpbin.org").clicked() {
|
|
|
|
*url = "https://httpbin.org/post".to_owned();
|
|
|
|
*method = Method::Post;
|
2020-11-18 20:38:29 +00:00
|
|
|
trigger_fetch = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-12-28 18:50:48 +00:00
|
|
|
if trigger_fetch {
|
2021-08-15 14:56:46 +00:00
|
|
|
Some(match *method {
|
|
|
|
Method::Get => Request::get(url),
|
|
|
|
Method::Post => Request::post(url, request_body),
|
|
|
|
})
|
2020-12-28 18:50:48 +00:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2020-11-18 20:38:29 +00:00
|
|
|
}
|
|
|
|
|
2021-02-03 19:04:57 +00:00
|
|
|
fn ui_resource(
|
2020-11-20 18:50:47 +00:00
|
|
|
ui: &mut egui::Ui,
|
2020-12-31 13:31:11 +00:00
|
|
|
frame: &mut epi::Frame<'_>,
|
2020-11-20 18:50:47 +00:00
|
|
|
tex_mngr: &mut TexMngr,
|
|
|
|
resource: &Resource,
|
|
|
|
) {
|
2020-12-28 18:50:48 +00:00
|
|
|
let Resource {
|
|
|
|
response,
|
2021-08-15 14:56:46 +00:00
|
|
|
text,
|
2020-12-28 18:50:48 +00:00
|
|
|
image,
|
|
|
|
colored_text,
|
|
|
|
} = resource;
|
2020-11-18 20:38:29 +00:00
|
|
|
|
|
|
|
ui.monospace(format!("url: {}", response.url));
|
2020-11-17 23:43:58 +00:00
|
|
|
ui.monospace(format!(
|
2020-11-18 20:38:29 +00:00
|
|
|
"status: {} ({})",
|
2020-11-17 23:43:58 +00:00
|
|
|
response.status, response.status_text
|
|
|
|
));
|
2020-11-18 20:38:29 +00:00
|
|
|
ui.monospace(format!(
|
2021-08-15 14:56:46 +00:00
|
|
|
"content-type: {}",
|
|
|
|
response.content_type().unwrap_or_default()
|
|
|
|
));
|
|
|
|
ui.monospace(format!(
|
|
|
|
"size: {:.1} kB",
|
2020-11-18 20:38:29 +00:00
|
|
|
response.bytes.len() as f32 / 1000.0
|
|
|
|
));
|
2020-11-17 23:43:58 +00:00
|
|
|
|
2021-08-15 14:56:46 +00:00
|
|
|
ui.separator();
|
|
|
|
|
|
|
|
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 {
|
2020-12-28 18:50:48 +00:00
|
|
|
let tooltip = "Click to copy the response body";
|
2021-01-25 17:50:19 +00:00
|
|
|
if ui.button("📋").on_hover_text(tooltip).clicked() {
|
2020-12-28 18:50:48 +00:00
|
|
|
ui.output().copied_text = text.clone();
|
2020-11-18 20:38:29 +00:00
|
|
|
}
|
2020-12-28 18:50:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ui.separator();
|
|
|
|
|
2021-08-28 11:18:21 +00:00
|
|
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
2020-12-28 18:50:48 +00:00
|
|
|
if let Some(image) = image {
|
2021-01-01 16:11:05 +00:00
|
|
|
if let Some(texture_id) = tex_mngr.texture(frame, &response.url, image) {
|
2020-12-28 18:50:48 +00:00
|
|
|
let size = egui::Vec2::new(image.size.0 as f32, image.size.1 as f32);
|
|
|
|
ui.image(texture_id, size);
|
|
|
|
}
|
|
|
|
} else if let Some(colored_text) = colored_text {
|
|
|
|
colored_text.ui(ui);
|
2021-08-15 14:56:46 +00:00
|
|
|
} else if let Some(text) = &text {
|
2020-11-18 20:38:29 +00:00
|
|
|
ui.monospace(text);
|
2020-12-28 18:50:48 +00:00
|
|
|
} else {
|
|
|
|
ui.monospace("[binary]");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Syntax highlighting:
|
|
|
|
|
2021-01-31 15:52:26 +00:00
|
|
|
#[cfg(feature = "syntect")]
|
2021-08-15 14:56:46 +00:00
|
|
|
fn syntax_highlighting(response: &Response, text: &str) -> Option<ColoredText> {
|
2020-12-28 18:50:48 +00:00
|
|
|
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
|
|
|
let extension = extension_and_rest.get(0)?;
|
|
|
|
ColoredText::text_with_extension(text, extension)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Lines of text fragments
|
2021-01-31 15:52:26 +00:00
|
|
|
#[cfg(feature = "syntect")]
|
2020-12-28 18:50:48 +00:00
|
|
|
struct ColoredText(Vec<Vec<(syntect::highlighting::Style, String)>>);
|
|
|
|
|
2021-01-31 15:52:26 +00:00
|
|
|
#[cfg(feature = "syntect")]
|
2020-12-28 18:50:48 +00:00
|
|
|
impl ColoredText {
|
2021-01-01 16:11:05 +00:00
|
|
|
/// e.g. `text_with_extension("fn foo() {}", "rs")`
|
2020-12-28 18:50:48 +00:00
|
|
|
pub fn text_with_extension(text: &str, extension: &str) -> Option<ColoredText> {
|
|
|
|
use syntect::easy::HighlightLines;
|
|
|
|
use syntect::highlighting::ThemeSet;
|
|
|
|
use syntect::parsing::SyntaxSet;
|
|
|
|
use syntect::util::LinesWithEndings;
|
|
|
|
|
|
|
|
let ps = SyntaxSet::load_defaults_newlines(); // should be cached and reused
|
|
|
|
let ts = ThemeSet::load_defaults(); // should be cached and reused
|
|
|
|
|
|
|
|
let syntax = ps.find_syntax_by_extension(extension)?;
|
|
|
|
|
|
|
|
let mut h = HighlightLines::new(syntax, &ts.themes["base16-mocha.dark"]);
|
|
|
|
|
2021-01-01 16:11:05 +00:00
|
|
|
let lines = LinesWithEndings::from(text)
|
2020-12-28 18:50:48 +00:00
|
|
|
.map(|line| {
|
|
|
|
h.highlight(line, &ps)
|
|
|
|
.into_iter()
|
|
|
|
.map(|(style, range)| (style, range.trim_end_matches('\n').to_owned()))
|
|
|
|
.collect()
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
Some(ColoredText(lines))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn ui(&self, ui: &mut egui::Ui) {
|
|
|
|
for line in &self.0 {
|
2021-03-21 13:44:59 +00:00
|
|
|
ui.horizontal_wrapped(|ui| {
|
|
|
|
ui.spacing_mut().item_spacing = egui::Vec2::ZERO;
|
|
|
|
ui.set_row_height(ui.fonts()[egui::TextStyle::Body].row_height());
|
|
|
|
|
2020-12-28 18:50:48 +00:00
|
|
|
for (style, range) in line {
|
|
|
|
let fg = style.foreground;
|
2021-01-02 16:02:18 +00:00
|
|
|
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
2020-12-28 18:50:48 +00:00
|
|
|
ui.add(egui::Label::new(range).monospace().text_color(text_color));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-11-18 20:38:29 +00:00
|
|
|
}
|
2020-11-17 23:43:58 +00:00
|
|
|
}
|
2020-11-20 18:50:47 +00:00
|
|
|
|
2021-01-31 15:52:26 +00:00
|
|
|
#[cfg(not(feature = "syntect"))]
|
2021-08-15 14:56:46 +00:00
|
|
|
fn syntax_highlighting(_: &Response, _: &str) -> Option<ColoredText> {
|
2021-01-31 15:52:26 +00:00
|
|
|
None
|
|
|
|
}
|
|
|
|
#[cfg(not(feature = "syntect"))]
|
|
|
|
struct ColoredText();
|
|
|
|
#[cfg(not(feature = "syntect"))]
|
|
|
|
impl ColoredText {
|
|
|
|
pub fn ui(&self, _ui: &mut egui::Ui) {}
|
|
|
|
}
|
|
|
|
|
2020-11-20 18:50:47 +00:00
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Texture/image handling is very manual at the moment.
|
|
|
|
|
2020-12-28 18:50:48 +00:00
|
|
|
/// Immediate mode texture manager that supports at most one texture at the time :)
|
2020-11-20 18:50:47 +00:00
|
|
|
#[derive(Default)]
|
|
|
|
struct TexMngr {
|
|
|
|
loaded_url: String,
|
|
|
|
texture_id: Option<egui::TextureId>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TexMngr {
|
|
|
|
fn texture(
|
|
|
|
&mut self,
|
2020-12-31 13:31:11 +00:00
|
|
|
frame: &mut epi::Frame<'_>,
|
2020-11-20 18:50:47 +00:00
|
|
|
url: &str,
|
|
|
|
image: &Image,
|
|
|
|
) -> Option<egui::TextureId> {
|
|
|
|
if self.loaded_url != url {
|
2021-01-07 15:29:58 +00:00
|
|
|
if let Some(texture_id) = self.texture_id.take() {
|
2021-02-28 18:09:48 +00:00
|
|
|
frame.tex_allocator().free(texture_id);
|
2021-01-07 15:29:58 +00:00
|
|
|
}
|
|
|
|
|
2021-02-28 18:09:48 +00:00
|
|
|
self.texture_id = Some(
|
|
|
|
frame
|
|
|
|
.tex_allocator()
|
|
|
|
.alloc_srgba_premultiplied(image.size, &image.pixels),
|
|
|
|
);
|
2020-11-20 18:50:47 +00:00
|
|
|
self.loaded_url = url.to_owned();
|
|
|
|
}
|
2021-01-07 15:29:58 +00:00
|
|
|
self.texture_id
|
2020-11-20 18:50:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Image {
|
|
|
|
size: (usize, usize),
|
2021-01-02 16:02:18 +00:00
|
|
|
pixels: Vec<egui::Color32>,
|
2020-11-20 18:50:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Image {
|
|
|
|
fn decode(bytes: &[u8]) -> Option<Image> {
|
|
|
|
use image::GenericImageView;
|
2021-01-01 16:11:05 +00:00
|
|
|
let image = image::load_from_memory(bytes).ok()?;
|
2020-11-28 10:06:39 +00:00
|
|
|
let image_buffer = image.to_rgba8();
|
2020-11-20 18:50:47 +00:00
|
|
|
let size = (image.width() as usize, image.height() as usize);
|
|
|
|
let pixels = image_buffer.into_vec();
|
|
|
|
assert_eq!(size.0 * size.1 * 4, pixels.len());
|
|
|
|
let pixels = pixels
|
|
|
|
.chunks(4)
|
2021-01-02 16:02:18 +00:00
|
|
|
.map(|p| egui::Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
|
2020-11-20 18:50:47 +00:00
|
|
|
.collect();
|
|
|
|
|
|
|
|
Some(Image { size, pixels })
|
|
|
|
}
|
|
|
|
}
|