henad-app 0.3.0

Desktop and web app for Henad, a parallel agent-based modelling engine.
Documentation
use eframe::egui::{
    self, Color32, CornerRadius, Margin, Stroke, Style, Vec2, Visuals,
    epaint::Shadow,
    style::{Interaction, ScrollStyle, Selection, Spacing, TextCursorStyle, WidgetVisuals, Widgets},
};
use eframe::egui_wgpu;
use henad_compute::gpu::GpuNeeds;

use crate::ui::mcs;

pub fn setup_custom_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();

    fonts.font_data.insert(
        "ibm_plex_sans_font".to_owned(),
        egui::FontData::from_static(include_bytes!("../assets/fonts/Henad Sans Regular.ttf")).into(),
    );
    fonts.font_data.insert(
        "ibm_plex_mono_font".to_owned(),
        egui::FontData::from_static(include_bytes!("../assets/fonts/Henad Mono Regular.ttf")).into(),
    );
    fonts.font_data.insert(
        "material_design_icons_font".to_owned(),
        egui::FontData::from_static(include_bytes!("../assets/fonts/Material Design Icons.ttf")).into(),
    );
    fonts.font_data.insert(
        "material_symbols_font".to_owned(),
        egui::FontData::from_static(include_bytes!("../assets/fonts/Material Symbols Outlined.ttf")).into(),
    );
    fonts
        .families
        .entry(egui::FontFamily::Proportional)
        .or_default()
        .insert(0, "ibm_plex_sans_font".to_owned());
    fonts
        .families
        .entry(egui::FontFamily::Proportional)
        .or_default()
        .insert(1, "material_design_icons_font".to_owned());
    fonts
        .families
        .entry(egui::FontFamily::Proportional)
        .or_default()
        .insert(2, "material_symbols_font".to_owned());

    fonts
        .families
        .entry(egui::FontFamily::Monospace)
        .or_default()
        .insert(0, "ibm_plex_mono_font".to_owned());

    ctx.set_fonts(fonts);
}

/// Returns the wgpu setup both entry points use, with a device sized to `needs`. Only the device request differs from
/// egui's default.
pub fn wgpu_configuration(needs: GpuNeeds) -> egui_wgpu::WgpuConfiguration {
    egui_wgpu::WgpuConfiguration {
        wgpu_setup: egui_wgpu::WgpuSetup::CreateNew(egui_wgpu::WgpuSetupCreateNew {
            device_descriptor: std::sync::Arc::new(move |adapter| device_descriptor(adapter, needs)),
            ..egui_wgpu::WgpuSetupCreateNew::without_display_handle()
        }),
        ..Default::default()
    }
}

/// Returns the device descriptor that Henad requests for models that need `needs`, added to egui's request.
///
/// `raise` clamps to the adapter's limits. On the web the adapter reports the browser's
/// ceiling, well under the hardware limits.
pub fn device_descriptor(adapter: &wgpu::Adapter, needs: GpuNeeds) -> wgpu::DeviceDescriptor<'static> {
    let base = egui_wgpu::WgpuSetupCreateNew::without_display_handle();
    let mut descriptor = (base.device_descriptor)(adapter);
    // Feeds the GPU time/step readout, "N/A" without it. Reading the timestamps back blocks, and
    // the browser's main thread must not block.
    #[cfg(not(target_arch = "wasm32"))]
    if adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY) {
        descriptor.required_features |= wgpu::Features::TIMESTAMP_QUERY;
    }
    descriptor.required_limits = henad_compute::gpu::limits::raise(adapter, &descriptor.required_limits, needs);
    descriptor
}

// Generated by egui-themer (https://github.com/grantshandy/egui-themer).

#[expect(clippy::too_many_lines, reason = "I don't care.")]
pub fn style() -> Style {
    Style {
        // override the text styles here:
        // override_text_style: Option<TextStyle>

        // override the font id here:
        // override_font_id: Option<FontId>

        // set your text styles here:
        // text_styles: BTreeMap<TextStyle, FontId>,

        // set your drag value text style:
        // drag_value_text_style: TextStyle,
        spacing: Spacing {
            item_spacing: Vec2 { x: 8.0, y: 3.0 },
            window_margin: Margin {
                left: 6,
                right: 6,
                top: 6,
                bottom: 6,
            },
            button_padding: Vec2 { x: 4.0, y: 1.0 },
            menu_margin: Margin {
                left: 6,
                right: 6,
                top: 6,
                bottom: 6,
            },
            indent: 18.0,
            interact_size: Vec2 { x: 40.0, y: 18.0 },
            slider_width: 100.0,
            combo_width: 100.0,
            text_edit_width: 280.0,
            icon_width: 14.0,
            icon_width_inner: 8.0,
            icon_spacing: 4.0,
            tooltip_width: 500.0,
            indent_ends_with_horizontal_line: false,
            combo_height: 200.0,
            scroll: ScrollStyle {
                bar_width: 10.0,
                handle_min_length: 12.0,
                bar_inner_margin: 4.0,
                bar_outer_margin: 0.0,
                ..Default::default()
            },
            ..Default::default()
        },
        interaction: Interaction {
            resize_grab_radius_side: 5.0,
            resize_grab_radius_corner: 10.0,
            show_tooltips_only_when_still: true,
            ..Default::default()
        },
        visuals: Visuals {
            dark_mode: true,
            override_text_color: None,
            widgets: Widgets {
                noninteractive: WidgetVisuals {
                    bg_fill: Color32::from_rgba_premultiplied(27, 27, 27, 255),
                    weak_bg_fill: Color32::from_rgba_premultiplied(27, 27, 27, 255),
                    bg_stroke: Stroke {
                        width: 1.0,
                        color: Color32::from_rgba_premultiplied(60, 60, 60, 255),
                    },
                    corner_radius: CornerRadius {
                        nw: 0,
                        ne: 0,
                        sw: 0,
                        se: 0,
                    },
                    fg_stroke: Stroke {
                        width: 1.0,
                        color: mcs::GRAY_300,
                    },
                    expansion: 0.0,
                },
                inactive: WidgetVisuals {
                    bg_fill: Color32::from_rgba_premultiplied(60, 60, 60, 255),
                    weak_bg_fill: Color32::from_rgba_premultiplied(60, 60, 60, 255),
                    bg_stroke: Stroke {
                        width: 0.0,
                        color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
                    },
                    corner_radius: CornerRadius {
                        nw: 0,
                        ne: 0,
                        sw: 0,
                        se: 0,
                    },
                    fg_stroke: Stroke {
                        width: 1.0,
                        color: mcs::GRAY_200,
                    },
                    expansion: 0.0,
                },
                hovered: WidgetVisuals {
                    bg_fill: Color32::from_rgba_premultiplied(70, 70, 70, 255),
                    weak_bg_fill: Color32::from_rgba_premultiplied(70, 70, 70, 255),
                    bg_stroke: Stroke {
                        width: 1.0,
                        color: Color32::from_rgba_premultiplied(150, 150, 150, 255),
                    },
                    corner_radius: CornerRadius {
                        nw: 0,
                        ne: 0,
                        sw: 0,
                        se: 0,
                    },
                    fg_stroke: Stroke {
                        width: 1.5,
                        color: mcs::GRAY_50,
                    },
                    expansion: 1.0,
                },
                active: WidgetVisuals {
                    bg_fill: Color32::from_rgba_premultiplied(55, 55, 55, 255),
                    weak_bg_fill: Color32::from_rgba_premultiplied(55, 55, 55, 255),
                    bg_stroke: Stroke {
                        width: 1.0,
                        color: Color32::from_rgba_premultiplied(255, 255, 255, 255),
                    },
                    corner_radius: CornerRadius {
                        nw: 0,
                        ne: 0,
                        sw: 0,
                        se: 0,
                    },
                    fg_stroke: Stroke {
                        width: 2.0,
                        color: Color32::from_rgba_premultiplied(255, 255, 255, 255),
                    },
                    expansion: 1.0,
                },
                open: WidgetVisuals {
                    bg_fill: Color32::from_rgba_premultiplied(27, 27, 27, 255),
                    weak_bg_fill: Color32::from_rgba_premultiplied(45, 45, 45, 255),
                    bg_stroke: Stroke {
                        width: 1.0,
                        color: Color32::from_rgba_premultiplied(60, 60, 60, 255),
                    },
                    corner_radius: CornerRadius {
                        nw: 0,
                        ne: 0,
                        sw: 0,
                        se: 0,
                    },
                    fg_stroke: Stroke {
                        width: 1.0,
                        color: Color32::from_rgba_premultiplied(210, 210, 210, 255),
                    },
                    expansion: 0.0,
                },
            },
            selection: Selection {
                bg_fill: mcs::BLUE_700,
                stroke: Stroke {
                    width: 1.0,
                    color: Color32::from_rgba_premultiplied(192, 222, 255, 255),
                },
            },
            hyperlink_color: mcs::BLUE_200,
            faint_bg_color: Color32::from_rgba_premultiplied(5, 5, 5, 0),
            extreme_bg_color: Color32::from_rgba_premultiplied(10, 10, 10, 255),
            code_bg_color: Color32::from_rgba_premultiplied(64, 64, 64, 255),
            warn_fg_color: mcs::ORANGE_500,
            error_fg_color: mcs::RED_500,
            window_corner_radius: CornerRadius {
                nw: 0,
                ne: 0,
                sw: 0,
                se: 0,
            },
            window_shadow: Shadow {
                spread: 0,
                color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
                blur: 15,
                offset: [10, 20],
            },
            window_fill: mcs::GRAY_900,
            window_stroke: Stroke {
                width: 1.0,
                color: mcs::GRAY_800,
            },
            menu_corner_radius: CornerRadius {
                nw: 0,
                ne: 0,
                sw: 0,
                se: 0,
            },
            panel_fill: Color32::from_rgba_premultiplied(21, 21, 21, 255),
            popup_shadow: Shadow {
                spread: 0,
                color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
                blur: 8,
                offset: [6, 10],
            },
            resize_corner_size: 12.0,
            text_cursor: TextCursorStyle {
                stroke: Stroke {
                    width: 2.0,
                    color: Color32::from_rgba_premultiplied(192, 222, 255, 255),
                },
                preview: false,
                ..Default::default()
            },
            button_frame: true,
            collapsing_header_frame: true,
            indent_has_left_vline: true,
            striped: false,
            slider_trailing_fill: true,
            ..Default::default()
        },
        animation_time: 0.083_333_336,
        explanation_tooltips: false,
        ..Default::default()
    }
}

pub fn setup_custom_styles(ctx: &egui::Context) {
    // egui 0.35 keeps a style per theme. Henad has one look, so both slots get it.
    let style = std::sync::Arc::new(style());
    ctx.set_style_of(egui::Theme::Dark, style.clone());
    ctx.set_style_of(egui::Theme::Light, style);
}