android: pick file
This commit is contained in:
parent
2fb3bffc8f
commit
d16624d423
7 changed files with 200 additions and 73 deletions
|
@ -1,6 +1,8 @@
|
||||||
package mw.gri.android;
|
package mw.gri.android;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.*;
|
import android.content.*;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
@ -14,6 +16,8 @@ import android.util.Size;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.camera.core.*;
|
import androidx.camera.core.*;
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||||
|
@ -26,7 +30,7 @@ import androidx.core.view.WindowInsetsCompat;
|
||||||
import com.google.androidgamesdk.GameActivity;
|
import com.google.androidgamesdk.GameActivity;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
@ -63,6 +67,9 @@ public class MainActivity extends GameActivity {
|
||||||
private ExecutorService mCameraExecutor = null;
|
private ExecutorService mCameraExecutor = null;
|
||||||
private boolean mUseBackCamera = true;
|
private boolean mUseBackCamera = true;
|
||||||
|
|
||||||
|
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null;
|
||||||
|
|
||||||
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
// Clear cache on start.
|
// Clear cache on start.
|
||||||
|
@ -84,6 +91,36 @@ public class MainActivity extends GameActivity {
|
||||||
// Register receiver to finish activity from the BackgroundService.
|
// Register receiver to finish activity from the BackgroundService.
|
||||||
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
||||||
|
|
||||||
|
// Register file pick result launcher.
|
||||||
|
mFilePickResultLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
int resultCode = result.getResultCode();
|
||||||
|
Intent data = result.getData();
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
String path = "";
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
String name = "pick" + Utils.getFileExtension(uri, this);
|
||||||
|
File file = new File(getExternalCacheDir(), name);
|
||||||
|
try (InputStream is = getContentResolver().openInputStream(uri);
|
||||||
|
OutputStream os = new FileOutputStream(file)) {
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int length;
|
||||||
|
while ((length = is.read(buffer)) > 0) {
|
||||||
|
os.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
path = file.getPath();
|
||||||
|
}
|
||||||
|
onFilePick(path);
|
||||||
|
} else {
|
||||||
|
onFilePick("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Listener for display insets (cutouts) to pass values into native code.
|
// Listener for display insets (cutouts) to pass values into native code.
|
||||||
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
||||||
|
@ -354,9 +391,23 @@ public class MainActivity extends GameActivity {
|
||||||
startActivity(Intent.createChooser(intent, "Share image"));
|
startActivity(Intent.createChooser(intent, "Share image"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if device is using dark theme.
|
// Called from native code to check if device is using dark theme.
|
||||||
public boolean useDarkTheme() {
|
public boolean useDarkTheme() {
|
||||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called from native code to pick the file.
|
||||||
|
public void pickFile() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
intent.setType("*/*");
|
||||||
|
try {
|
||||||
|
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
|
||||||
|
} catch (android.content.ActivityNotFoundException ex) {
|
||||||
|
onFilePick("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass picked file into native code.
|
||||||
|
public native void onFilePick(String path);
|
||||||
}
|
}
|
|
@ -5,6 +5,8 @@ import android.graphics.ImageFormat;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.YuvImage;
|
import android.graphics.YuvImage;
|
||||||
import android.media.Image;
|
import android.media.Image;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
import androidx.camera.core.ImageProxy;
|
import androidx.camera.core.ImageProxy;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -146,4 +148,9 @@ public class Utils {
|
||||||
}
|
}
|
||||||
return directoryToBeDeleted.delete();
|
return directoryToBeDeleted.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getFileExtension(Uri uri, Context context) {
|
||||||
|
String fileType = context.getContentResolver().getType(uri);
|
||||||
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,13 +42,13 @@ impl Android {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call Android Activity method with JNI.
|
/// Call Android Activity method with JNI.
|
||||||
pub fn call_java_method(&self, name: &str, sig: &str, args: &[JValue]) -> Option<jni::sys::jvalue> {
|
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::jvalue> {
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
let mut env = vm.attach_current_thread().unwrap();
|
let mut env = vm.attach_current_thread().unwrap();
|
||||||
let activity = unsafe {
|
let activity = unsafe {
|
||||||
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
||||||
};
|
};
|
||||||
if let Ok(result) = env.call_method(activity, name, sig, args) {
|
if let Ok(result) = env.call_method(activity, name, s, a) {
|
||||||
return Some(result.as_jni().clone());
|
return Some(result.as_jni().clone());
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
@ -145,6 +145,26 @@ impl PlatformCallbacks for Android {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pick_file(&self) -> Option<String> {
|
fn pick_file(&self) -> Option<String> {
|
||||||
|
// Clear previous result.
|
||||||
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
|
*w_path = None;
|
||||||
|
// Launch file picker.
|
||||||
|
let _ = self.call_java_method("pickFile", "()V", &[]).unwrap();
|
||||||
|
// Return empty string to identify async pick.
|
||||||
|
Some("".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn picked_file(&self) -> Option<String> {
|
||||||
|
let has_file = {
|
||||||
|
let r_path = PICKED_FILE_PATH.read();
|
||||||
|
r_path.is_some()
|
||||||
|
};
|
||||||
|
if has_file {
|
||||||
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
|
let path = Some(w_path.clone().unwrap());
|
||||||
|
*w_path = None;
|
||||||
|
return path
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,13 +172,13 @@ impl PlatformCallbacks for Android {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Last image data from camera.
|
/// Last image data from camera.
|
||||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
||||||
|
/// Picked file path.
|
||||||
|
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
/// Callback from Java code with last entered character from soft keyboard.
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
/// Callback from Java code with last entered character from soft keyboard.
|
|
||||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JObject,
|
_class: JObject,
|
||||||
|
@ -169,4 +189,26 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
||||||
let image : Vec<u8> = env.convert_byte_array(arr).unwrap();
|
let image : Vec<u8> = env.convert_byte_array(arr).unwrap();
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
*w_image = Some((image, rotation as u32));
|
*w_image = Some((image, rotation as u32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callback from Java code with picked file path.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick(
|
||||||
|
_env: JNIEnv,
|
||||||
|
_class: JObject,
|
||||||
|
char: jni::sys::jstring
|
||||||
|
) {
|
||||||
|
use std::ops::Add;
|
||||||
|
unsafe {
|
||||||
|
let j_obj = JString::from_raw(char);
|
||||||
|
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
|
||||||
|
match j_str.to_str() {
|
||||||
|
Ok(str) => {
|
||||||
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
|
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -153,6 +153,10 @@ impl PlatformCallbacks for Desktop {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn picked_file(&self) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
|
|
@ -33,4 +33,5 @@ pub trait PlatformCallbacks {
|
||||||
fn switch_camera(&self);
|
fn switch_camera(&self);
|
||||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
||||||
fn pick_file(&self) -> Option<String>;
|
fn pick_file(&self) -> Option<String>;
|
||||||
|
fn picked_file(&self) -> Option<String>;
|
||||||
}
|
}
|
|
@ -18,12 +18,14 @@ use std::{fs, thread};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::FILE_ARROW_UP;
|
use crate::gui::icons::ARCHIVE_BOX;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::View;
|
use crate::gui::views::View;
|
||||||
|
|
||||||
/// Button to pick file and parse its data into text.
|
/// Button to pick file and parse its data into text.
|
||||||
pub struct FilePickButton {
|
pub struct FilePickButton {
|
||||||
|
/// Flag to check if file is picking.
|
||||||
|
pub file_picking: Arc<AtomicBool>,
|
||||||
/// Flag to check if file is parsing.
|
/// Flag to check if file is parsing.
|
||||||
pub file_parsing: Arc<AtomicBool>,
|
pub file_parsing: Arc<AtomicBool>,
|
||||||
/// File parsing result.
|
/// File parsing result.
|
||||||
|
@ -33,6 +35,7 @@ pub struct FilePickButton {
|
||||||
impl Default for FilePickButton {
|
impl Default for FilePickButton {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
file_picking: Arc::new(AtomicBool::new(false)),
|
||||||
file_parsing: Arc::new(AtomicBool::new(false)),
|
file_parsing: Arc::new(AtomicBool::new(false)),
|
||||||
file_parsing_result: Arc::new(RwLock::new(None))
|
file_parsing_result: Arc::new(RwLock::new(None))
|
||||||
}
|
}
|
||||||
|
@ -45,8 +48,16 @@ impl FilePickButton {
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
cb: &dyn PlatformCallbacks,
|
cb: &dyn PlatformCallbacks,
|
||||||
on_result: impl FnOnce(String)) {
|
on_result: impl FnOnce(String)) {
|
||||||
if self.file_parsing.load(Ordering::Relaxed) {
|
if self.file_picking.load(Ordering::Relaxed) {
|
||||||
// Draw loading spinner on file parsing.
|
View::small_loading_spinner(ui);
|
||||||
|
// Check file pick result.
|
||||||
|
if let Some(path) = cb.picked_file() {
|
||||||
|
self.file_picking.store(false, Ordering::Relaxed);
|
||||||
|
if !path.is_empty() {
|
||||||
|
self.on_file_pick(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if self.file_parsing.load(Ordering::Relaxed) {
|
||||||
View::small_loading_spinner(ui);
|
View::small_loading_spinner(ui);
|
||||||
// Check file parsing result.
|
// Check file parsing result.
|
||||||
let has_result = {
|
let has_result = {
|
||||||
|
@ -67,29 +78,39 @@ impl FilePickButton {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Draw button to pick file.
|
// Draw button to pick file.
|
||||||
let file_text = format!("{} {}", FILE_ARROW_UP, t!("choose_file"));
|
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||||
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
|
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
|
||||||
if let Some(path) = cb.pick_file() {
|
if let Some(path) = cb.pick_file() {
|
||||||
// Parse file at new thread.
|
self.on_file_pick(path);
|
||||||
self.file_parsing.store(true, Ordering::Relaxed);
|
|
||||||
let result = self.file_parsing_result.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
if path.ends_with(".gif") {
|
|
||||||
//TODO: Detect QR codes on GIF file.
|
|
||||||
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") ||
|
|
||||||
path.ends_with(".png") {
|
|
||||||
//TODO: Detect QR codes on image files.
|
|
||||||
} else {
|
|
||||||
// Parse file as plain text.
|
|
||||||
if let Ok(text) = fs::read_to_string(path) {
|
|
||||||
let mut w_res = result.write();
|
|
||||||
*w_res = Some(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle picked file path.
|
||||||
|
fn on_file_pick(&self, path: String) {
|
||||||
|
// Wait for asynchronous file pick result if path is empty.
|
||||||
|
if path.is_empty() {
|
||||||
|
self.file_picking.store(true, Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.file_parsing.store(true, Ordering::Relaxed);
|
||||||
|
let result = self.file_parsing_result.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
if path.ends_with(".gif") {
|
||||||
|
//TODO: Detect QR codes on GIF file.
|
||||||
|
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") ||
|
||||||
|
path.ends_with(".png") {
|
||||||
|
//TODO: Detect QR codes on image files.
|
||||||
|
} else {
|
||||||
|
// Parse file as plain text.
|
||||||
|
let mut w_res = result.write();
|
||||||
|
if let Ok(text) = fs::read_to_string(path) {
|
||||||
|
*w_res = Some(text);
|
||||||
|
} else {
|
||||||
|
*w_res = Some("".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -73,50 +73,6 @@ impl QrCodeContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw QR code image content.
|
|
||||||
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.min += egui::emath::vec2(10.0, 0.0);
|
|
||||||
rect.max -= egui::emath::vec2(10.0, 0.0);
|
|
||||||
|
|
||||||
// Create background shape.
|
|
||||||
let mut bg_shape = egui::epaint::RectShape {
|
|
||||||
rect,
|
|
||||||
rounding: egui::Rounding::default(),
|
|
||||||
fill: egui::Color32::WHITE,
|
|
||||||
stroke: egui::Stroke::NONE,
|
|
||||||
fill_texture_id: Default::default(),
|
|
||||||
uv: egui::Rect::ZERO
|
|
||||||
};
|
|
||||||
let bg_idx = ui.painter().add(bg_shape);
|
|
||||||
|
|
||||||
// Draw QR code image content.
|
|
||||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
|
||||||
ui.add_space(10.0);
|
|
||||||
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
|
||||||
let color_img = load_svg_bytes_with_size(svg.as_slice(), Some(size)).unwrap();
|
|
||||||
// Create image texture.
|
|
||||||
let texture_handle = ui.ctx().load_texture("qr_code",
|
|
||||||
color_img.clone(),
|
|
||||||
TextureOptions::default());
|
|
||||||
self.texture_handle = Some(texture_handle.clone());
|
|
||||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
|
||||||
color_img.height() as f32);
|
|
||||||
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
|
|
||||||
// Add image to content.
|
|
||||||
ui.add(egui::Image::from_texture(sized_img)
|
|
||||||
.max_height(ui.available_width())
|
|
||||||
.fit_to_original_size(1.0));
|
|
||||||
ui.add_space(10.0);
|
|
||||||
}).response.rect;
|
|
||||||
|
|
||||||
// Setup background shape to be painted behind content.
|
|
||||||
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
|
||||||
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
|
||||||
bg_shape.rect = content_rect;
|
|
||||||
ui.painter().set(bg_idx, bg_shape);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw animated QR code content.
|
/// Draw animated QR code content.
|
||||||
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
||||||
if !self.has_image() {
|
if !self.has_image() {
|
||||||
|
@ -180,8 +136,9 @@ impl QrCodeContent {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add_space(8.0);
|
ui.add_space(2.0);
|
||||||
View::small_loading_spinner(ui);
|
View::small_loading_spinner(ui);
|
||||||
|
ui.add_space(1.0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,6 +228,50 @@ impl QrCodeContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw QR code image content.
|
||||||
|
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.min += egui::emath::vec2(10.0, 0.0);
|
||||||
|
rect.max -= egui::emath::vec2(10.0, 0.0);
|
||||||
|
|
||||||
|
// Create background shape.
|
||||||
|
let mut bg_shape = egui::epaint::RectShape {
|
||||||
|
rect,
|
||||||
|
rounding: egui::Rounding::default(),
|
||||||
|
fill: egui::Color32::WHITE,
|
||||||
|
stroke: egui::Stroke::NONE,
|
||||||
|
fill_texture_id: Default::default(),
|
||||||
|
uv: egui::Rect::ZERO
|
||||||
|
};
|
||||||
|
let bg_idx = ui.painter().add(bg_shape);
|
||||||
|
|
||||||
|
// Draw QR code image content.
|
||||||
|
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
||||||
|
let color_img = load_svg_bytes_with_size(svg.as_slice(), Some(size)).unwrap();
|
||||||
|
// Create image texture.
|
||||||
|
let texture_handle = ui.ctx().load_texture("qr_code",
|
||||||
|
color_img.clone(),
|
||||||
|
TextureOptions::default());
|
||||||
|
self.texture_handle = Some(texture_handle.clone());
|
||||||
|
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||||
|
color_img.height() as f32);
|
||||||
|
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
|
||||||
|
// Add image to content.
|
||||||
|
ui.add(egui::Image::from_texture(sized_img)
|
||||||
|
.max_height(ui.available_width())
|
||||||
|
.fit_to_original_size(1.0));
|
||||||
|
ui.add_space(10.0);
|
||||||
|
}).response.rect;
|
||||||
|
|
||||||
|
// Setup background shape to be painted behind content.
|
||||||
|
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
||||||
|
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
||||||
|
bg_shape.rect = content_rect;
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if QR code is loading.
|
/// Check if QR code is loading.
|
||||||
fn loading(&self) -> bool {
|
fn loading(&self) -> bool {
|
||||||
let r_state = self.qr_image_state.read();
|
let r_state = self.qr_image_state.read();
|
||||||
|
|
Loading…
Reference in a new issue