// [NUBCO BY FIPS @ 4FIPS.COM, (c) 2021 FILIP STOKLAS, MIT-LICENSED]
use eframe::{egui, epi};
use std::mem::size_of;
use crate::conv::*;
use urlencoding::encode as encode_url;
#[derive(Clone, Copy, Debug, PartialEq)]
enum Base {
Hex = 16,
Dec = 10,
Oct = 8,
Bin = 2,
}
struct ConversionTexts {
uhex: ConversionText,
udec: ConversionText,
idec: ConversionText,
uoct: ConversionText,
ubin: ConversionText,
}
#[derive(Default)]
struct ConversionTargets {
uhex: String,
udec: String,
idec: String,
uoct: String,
ubin: String,
}
#[derive(Default)]
struct State {
targets8: ConversionTargets,
targets16: ConversionTargets,
targets32: ConversionTargets,
targets64: ConversionTargets,
targets128: ConversionTargets,
}
pub struct NubcoApp {
app_name: String,
is_first_update: bool,
input: String,
base: Base,
state: State,
}
impl Default for NubcoApp {
fn default() -> Self {
Self {
app_name: format!("n u b c o (apps.4fips.com/nubco) v{}", env!("CARGO_PKG_VERSION")),
is_first_update: true,
input: "beef".to_owned(),
base: Base::Hex,
state: State::default(),
}
}
}
impl epi::App for NubcoApp {
fn name(&self) -> &str {
&self.app_name
}
fn setup(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>, _storage: Option<&dyn epi::Storage>) {
ctx.set_visuals(egui::Visuals::dark());
let scale = if frame.is_web() { 1.5 } else { 1.0 };
setup_style(ctx, scale);
setup_fonts(ctx, scale);
}
fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
let NubcoApp {
app_name: _,
is_first_update,
input,
base,
state,
} = self;
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
egui::Frame::default()
.margin(egui::vec2(2.0, 2.0))
.show(ui, |ui| {
about_panel(ui);
//debug_panel(ui, input, base);
input_panel(ui, input, base, *is_first_update);
conversion_panel(ui, input, base, state);
});
});
*is_first_update = false;
});
}
}
fn input_panel(ui: &mut egui::Ui, input: &mut String, base: &mut Base, grab_focus: bool) {
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.add(egui::Label::new("Input:").heading().strong());
let color = if ui.visuals().dark_mode { egui::Color32::YELLOW } else { egui::Color32::RED };
let response = ui.add(egui::TextEdit::singleline(input).text_color(color).text_style(egui::TextStyle::Heading).desired_width(f32::INFINITY));
if grab_focus {
ui.memory().request_focus(response.id);
}
});
ui.horizontal(|ui| {
ui.label("Base:");
ui.radio_value(base, Base::Hex, "hex");
ui.radio_value(base, Base::Dec, "dec");
ui.radio_value(base, Base::Oct, "oct");
ui.radio_value(base, Base::Bin, "bin");
});
ui.add_space(6.0);
}
fn conversion_panel(ui: &mut egui::Ui, input: &mut String, base: &mut Base, state: &mut State) {
let base = *base as u32;
let trimmed_input = input.trim();
if trimmed_input.is_empty() {
return;
}
#[derive(Default)]
struct Folding {
is_last_item: bool,
ok_unfolded: bool,
}
let mut folding = Folding::default();
macro_rules! conversion_item {
($ui: expr, $U: ty, $I: ty, $value: expr, $base: expr, $targets: expr, $folding: expr) => {
let ures = <$U>::from_str_radix($value, $base);
let ires = <$I>::from_str_radix($value, $base);
let ok = ures.is_ok() && ires.is_ok();
let warn = ures.is_ok() || ires.is_ok();
let symb = if ok { '✅' } else if warn { '❗' } else { '❌' };
let texts = ConversionTexts {
uhex: ConversionText::from(convert_ui(&ures, &ires, &val_to_hex)),
udec: ConversionText::from(convert_ui(&ures, &ires, &val_to_dec)),
idec: ConversionText::from(convert_iu(&ires, &ures, &val_to_dec)),
uoct: ConversionText::from(convert_ui(&ures, &ires, &val_to_oct)),
ubin: ConversionText::from(convert_ui(&ures, &ires, &val_to_bin)),
};
let nbits = (8 * size_of::<$U>()).to_string();
let expand = !$folding.ok_unfolded && (ok || warn || $folding.is_last_item);
conversion_item($ui, texts, $targets, &nbits, symb, expand);
$folding.ok_unfolded |= ok;
};
}
conversion_item!(ui, u8, i8, trimmed_input, base, &mut state.targets8, &mut folding);
conversion_item!(ui, u16, i16, trimmed_input, base, &mut state.targets16, &mut folding);
conversion_item!(ui, u32, i32, trimmed_input, base, &mut state.targets32, &mut folding);
conversion_item!(ui, u64, i64, trimmed_input, base, &mut state.targets64, &mut folding);
folding.is_last_item = true;
conversion_item!(ui, u128, i128, trimmed_input, base, &mut state.targets128, &mut folding);
}
fn conversion_item(ui: &mut egui::Ui, texts: ConversionTexts, targets: &mut ConversionTargets, nbits: &str, symb: char, expand: bool) {
egui::CollapsingHeader::new(format!("{} BIT {}{}", nbits, symb, if expand { " " } else { "" }))
.default_open(expand)
.show(ui, |ui| {
egui::Grid::new(format!("grid_{}", nbits))
.num_columns(2)
.show(ui, |ui| {
ui.label(format!("UINT{} (hex):", nbits));
targets.uhex = texts.uhex.text;
ui.add(egui::TextEdit::singleline(&mut targets.uhex).desired_width(ui.available_width() - 60.0));
if texts.uhex.symb.is_empty() { ui.label(texts.uhex.symb) } else { ui.label(texts.uhex.symb).on_hover_text(texts.uhex.symb_hover) };
ui.end_row();
ui.label(format!("UINT{} (dec):", nbits));
targets.udec = texts.udec.text;
ui.add(egui::TextEdit::singleline(&mut targets.udec).desired_width(ui.available_width() - 60.0));
if texts.udec.symb.is_empty() { ui.label(texts.udec.symb) } else { ui.label(texts.udec.symb).on_hover_text(texts.udec.symb_hover) };
ui.end_row();
ui.label(format!("INT{} (dec):", nbits));
targets.idec = texts.idec.text;
ui.add(egui::TextEdit::singleline(&mut targets.idec).desired_width(ui.available_width() - 60.0));
if texts.idec.symb.is_empty() { ui.label(texts.idec.symb) } else { ui.label(texts.idec.symb).on_hover_text(texts.idec.symb_hover) };
ui.end_row();
ui.label(format!("UINT{} (oct):", nbits));
targets.uoct = texts.uoct.text;
ui.add(egui::TextEdit::singleline(&mut targets.uoct).desired_width(ui.available_width() - 60.0));
if texts.uoct.symb.is_empty() { ui.label(texts.uoct.symb) } else { ui.label(texts.uoct.symb).on_hover_text(texts.uoct.symb_hover) };
ui.end_row();
ui.label(format!("UINT{} (bin):", nbits));
targets.ubin = texts.ubin.text;
ui.add(egui::TextEdit::singleline(&mut targets.ubin).desired_width(ui.available_width() - 60.0));
if texts.ubin.symb.is_empty() { ui.label(texts.ubin.symb) } else { ui.label(texts.ubin.symb).on_hover_text(texts.ubin.symb_hover) };
ui.end_row();
});
});
}
fn about_panel(ui: &mut egui::Ui) {
egui::CollapsingHeader::new("About")
.default_open(false)
.show(ui, |ui| {
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
let nubco_url = "https://apps.4fips.com/nubco";
ui.add(egui::Label::new(format!("n u b c o v{}", env!("CARGO_PKG_VERSION"))).heading().strong());
ui.label("a numeric base converter");
ui.label("brought to you for free by fips");
ui.label("(c) 2021 filip stoklas");
ui.hyperlink_to("apps.4fips.com/nubco", nubco_url);
ui.add_space(12.0);
ui.label("powered by:");
ui.hyperlink_to("Rust", "https://www.rust-lang.org");
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
ui.add_space(12.0);
ui.label("support this app by sharing it on:");
let share_text = "n u b c o ⭐ a numeric base converter ⭐ brought to you for free by fips ⭐ powered by Rust & egui";
let share_text_with_tags = format!("{} ⭐ #rustlang #egui ⭐", share_text);
ui.hyperlink_to("Twitter", share_on_twitter_url(nubco_url, &share_text_with_tags, "4fips"));
ui.hyperlink_to("Facebook", share_on_facebook_url(nubco_url));
ui.hyperlink_to("Reddit", share_on_reddit_url(nubco_url, share_text));
ui.hyperlink_to("Hacker News", share_on_hackernews_url(nubco_url, share_text));
});
});
}
#[allow(dead_code)]
fn debug_panel(ui: &mut egui::Ui, input: &mut String, base: &mut Base) {
egui::CollapsingHeader::new("Debug")
.default_open(false)
.show(ui, |ui| {
ui.horizontal(|ui| {
fn item(ui: &mut egui::Ui, input: &mut String, input_base: &mut Base, label: &str, value: &str, base: Base) {
if ui.add(egui::Button::new(label)).clicked() {
*input = value.to_owned();
*input_base = base;
}
}
ui.label("Debug:");
item(ui, input, base, "8", "ff", Base::Hex);
item(ui, input, base, "16", "ffff", Base::Hex);
item(ui, input, base, "32", "ffffffff", Base::Hex);
item(ui, input, base, "64", "ffffffffffffffff", Base::Hex);
item(ui, input, base, "128", "ffffffffffffffffffffffffffffffff", Base::Hex);
item(ui, input, base, "H1", "e00002d6", Base::Hex);
item(ui, input, base, "H2", "0xe00002d6", Base::Hex);
item(ui, input, base, "D1", "-536870186", Base::Dec);
item(ui, input, base, "-1", "-1", Base::Dec);
});
});
}
fn share_on_twitter_url(url: &str, text: &str, via: &str) -> String {
return format!("https://twitter.com/intent/tweet/?url={}&text={}&via={}", encode_url(url), encode_url(text), via);
}
fn share_on_facebook_url(url: &str) -> String {
return format!("https://facebook.com/sharer/sharer.php?u={}", encode_url(url));
}
fn share_on_reddit_url(url: &str, text: &str) -> String {
return format!("https://reddit.com/submit/?url={}&resubmit=true&title={}", encode_url(url), encode_url(text));
}
fn share_on_hackernews_url(url: &str, text: &str) -> String {
return format!("https://news.ycombinator.com/submitlink?u={}&t={}", encode_url(url), encode_url(text));
}
fn setup_style(ctx: &egui::CtxRef, scale: f32) {
let mut style = (*ctx.style()).clone();
{
macro_rules! corner_radius {
($varinat: ident, $value: expr) => {
style.visuals.widgets.$varinat.corner_radius = $value;
};
}
let radius = 6.0 * scale;
corner_radius!(noninteractive, radius);
corner_radius!(inactive, radius);
corner_radius!(hovered, radius);
corner_radius!(active, radius);
corner_radius!(open, radius);
}
if scale > 1.0 {
macro_rules! expansion {
($varinat: ident, $value: expr) => {
style.visuals.widgets.$varinat.expansion = $value;
};
}
let expansion = 5.0 * scale - 5.0;
expansion!(noninteractive, expansion);
expansion!(inactive, expansion);
expansion!(hovered, expansion * 1.3);
expansion!(active, expansion * 1.3);
expansion!(open, expansion * 1.3);
style.spacing.indent = 25.0;
}
style.spacing.item_spacing = egui::vec2(8.0 * scale, 8.0 * scale);
style.visuals.collapsing_header_frame = true;
//style.visuals.text_cursor_preview = true;
//style.spacing.indent_ends_with_horizontal_line = true;
ctx.set_style(style);
}
fn setup_fonts(ctx: &egui::CtxRef, scale: f32) {
let mut fonts = egui::FontDefinitions::default();
let font_size = 20.0 * scale;
fonts.family_and_size.insert(egui::TextStyle::Heading, (egui::FontFamily::Proportional, font_size));
fonts.family_and_size.insert(egui::TextStyle::Button, (egui::FontFamily::Proportional, font_size));
fonts.family_and_size.insert(egui::TextStyle::Body, (egui::FontFamily::Proportional, font_size));
ctx.set_fonts(fonts);
}