Unify context menu / hover tooltip code + UI improvements

This commit is contained in:
Luke Street
2025-03-02 16:13:12 -07:00
parent 8461b35cd7
commit a1f2a535e5
26 changed files with 1730 additions and 1095 deletions
+2 -2
View File
@@ -43,7 +43,7 @@ use crate::{
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState, View},
symbol_diff::{DiffViewAction, DiffViewState, ResolvedNavigation, View},
},
};
@@ -762,7 +762,7 @@ impl eframe::App for App {
ui.separator();
if ui.button("Clear custom symbol mappings").clicked() {
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
diff_state.post_build_nav = Some(ResolvedNavigation::default());
state.queue_reload = true;
}
});
+1 -2
View File
@@ -25,8 +25,7 @@ use tracing_subscriber::EnvFilter;
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
fn load_icon() -> Result<egui::IconData> {
use bytes::Buf;
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").reader());
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").as_ref());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
+157 -58
View File
@@ -1,8 +1,11 @@
use egui::{Id, Layout, RichText, ScrollArea, TextEdit, Ui, Widget};
use egui::{text::LayoutJob, Id, Layout, RichText, ScrollArea, TextEdit, Ui, Widget};
use objdiff_core::{
build::BuildStatus,
diff::{display::SymbolFilter, DiffObjConfig, ObjectDiff, SectionDiff, SymbolDiff},
obj::{Object, Section, SectionKind, Symbol},
diff::{
display::{ContextItem, HoverItem, HoverItemColor, SymbolFilter, SymbolNavigationKind},
DiffObjConfig, ObjectDiff, SectionDiff, SymbolDiff,
},
obj::{Object, Section, Symbol},
};
use time::format_description;
@@ -15,9 +18,11 @@ use crate::{
extab_diff::extab_ui,
function_diff::{asm_col_ui, FunctionDiffContext},
symbol_diff::{
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation,
DiffViewState, SymbolDiffContext, SymbolRefByName, View,
match_color_for_symbol, symbol_context_menu_ui, symbol_hover_ui, symbol_list_ui,
DiffViewAction, DiffViewNavigation, DiffViewState, SymbolDiffContext, SymbolRefByName,
View,
},
write_text,
},
};
@@ -112,21 +117,19 @@ pub fn diff_view_ui(
// Check if we need to perform any navigation
let current_navigation = DiffViewNavigation {
view: state.current_view,
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: state.symbol_state.right_symbol.clone(),
kind: match state.current_view {
View::ExtabDiff => SymbolNavigationKind::Extab,
_ => SymbolNavigationKind::Normal,
},
left_symbol: left_ctx.symbol.map(|(_, _, idx)| idx),
right_symbol: right_ctx.symbol.map(|(_, _, idx)| idx),
};
let mut navigation = current_navigation.clone();
if let Some((_symbol, symbol_diff, _symbol_idx)) = left_ctx.symbol {
// If a matching symbol appears, select it
if !right_ctx.has_symbol() {
if let Some(target_symbol_ref) = symbol_diff.target_symbol {
let (right_obj, _) = right_ctx.obj.unwrap();
let target_symbol = &right_obj.symbols[target_symbol_ref];
let target_section = target_symbol
.section
.and_then(|section_idx| right_obj.sections.get(section_idx));
navigation.right_symbol = Some(SymbolRefByName::new(target_symbol, target_section));
navigation.right_symbol = Some(target_symbol_ref);
}
}
} else if navigation.left_symbol.is_some()
@@ -140,12 +143,7 @@ pub fn diff_view_ui(
// If a matching symbol appears, select it
if !left_ctx.has_symbol() {
if let Some(target_symbol_ref) = symbol_diff.target_symbol {
let (left_obj, _) = left_ctx.obj.unwrap();
let target_symbol = &left_obj.symbols[target_symbol_ref];
let target_section = target_symbol
.section
.and_then(|section_idx| left_obj.sections.get(section_idx));
navigation.left_symbol = Some(SymbolRefByName::new(target_symbol, target_section));
navigation.left_symbol = Some(target_symbol_ref);
}
}
} else if navigation.right_symbol.is_some()
@@ -157,7 +155,7 @@ pub fn diff_view_ui(
}
// If both sides are missing a symbol, switch to symbol diff view
if navigation.left_symbol.is_none() && navigation.right_symbol.is_none() {
navigation.view = View::SymbolDiff;
navigation = DiffViewNavigation::default();
}
// Execute navigation if it changed
if navigation != current_navigation && state.post_build_nav.is_none() {
@@ -177,7 +175,7 @@ pub fn diff_view_ui(
} else {
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::default()));
}
if let Some((symbol, _, _)) = left_ctx.symbol {
@@ -220,12 +218,12 @@ pub fn diff_view_ui(
.color(appearance.replace_color),
);
}
} else if let Some((symbol, _, _)) = left_ctx.symbol {
ui.label(
RichText::new(symbol.demangled_name.as_deref().unwrap_or(&symbol.name))
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else if let Some((symbol, _symbol_diff, symbol_idx)) = left_ctx.symbol {
if let Some(action) =
symbol_label_ui(ui, left_ctx, symbol, symbol_idx, column, appearance)
{
ret = Some(action);
}
} else if let Some((section, _, _)) = left_ctx.section {
ui.label(
RichText::new(section.name.clone())
@@ -341,12 +339,12 @@ pub fn diff_view_ui(
.color(appearance.replace_color),
);
}
} else if let Some((symbol, _, _)) = right_ctx.symbol {
ui.label(
RichText::new(symbol.demangled_name.as_deref().unwrap_or(&symbol.name))
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else if let Some((symbol, _symbol_diff, symbol_idx)) = right_ctx.symbol {
if let Some(action) =
symbol_label_ui(ui, right_ctx, symbol, symbol_idx, column, appearance)
{
ret = Some(action);
}
} else if let Some((section, _, _)) = right_ctx.section {
ui.label(
RichText::new(section.name.clone())
@@ -573,6 +571,38 @@ pub fn diff_view_ui(
ret
}
fn symbol_label_ui(
ui: &mut Ui,
ctx: DiffColumnContext,
symbol: &Symbol,
symbol_idx: usize,
column: usize,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let (obj, diff) = ctx.obj.unwrap();
let ctx = SymbolDiffContext { obj, diff };
let mut ret = None;
egui::Label::new(
RichText::new(symbol.demangled_name.as_deref().unwrap_or(&symbol.name))
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
)
.selectable(false)
// TODO .show_tooltip_when_elided(false)
// https://github.com/emilk/egui/commit/071e090e2b2601e5ed4726a63a753188503dfaf2
.ui(ui)
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, ctx, symbol_idx, appearance))
.context_menu(|ui| {
let section = symbol.section.and_then(|section_idx| ctx.obj.sections.get(section_idx));
if let Some(result) =
symbol_context_menu_ui(ui, ctx, symbol_idx, symbol, section, column, appearance)
{
ret = Some(result);
}
});
ret
}
#[must_use]
fn diff_col_ui(
ui: &mut Ui,
@@ -641,15 +671,11 @@ fn diff_col_ui(
});
},
);
} else if let (
Some((other_section, _other_section_diff, _other_section_idx)),
Some((other_symbol, _other_symbol_diff, other_symbol_idx)),
) = (other_ctx.section, other_ctx.symbol)
} else if let Some((_other_symbol, _other_symbol_diff, other_symbol_idx)) = other_ctx.symbol
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj, diff },
None,
&state.symbol_state,
SymbolFilter::Mapping(other_symbol_idx, None),
appearance,
@@ -660,34 +686,20 @@ fn diff_col_ui(
(
0,
DiffViewAction::Navigate(DiffViewNavigation {
left_symbol: Some(left_symbol_ref),
left_symbol: Some(symbol_idx),
..
}),
) => {
ret = Some(DiffViewAction::SetMapping(
match other_section.kind {
SectionKind::Code => View::FunctionDiff,
_ => View::SymbolDiff,
},
left_symbol_ref,
SymbolRefByName::new(other_symbol, Some(other_section)),
));
ret = Some(DiffViewAction::SetMapping(symbol_idx, other_symbol_idx));
}
(
1,
DiffViewAction::Navigate(DiffViewNavigation {
right_symbol: Some(right_symbol_ref),
right_symbol: Some(symbol_idx),
..
}),
) => {
ret = Some(DiffViewAction::SetMapping(
match other_section.kind {
SectionKind::Code => View::FunctionDiff,
_ => View::SymbolDiff,
},
SymbolRefByName::new(other_symbol, Some(other_section)),
right_symbol_ref,
));
ret = Some(DiffViewAction::SetMapping(other_symbol_idx, symbol_idx));
}
(_, action) => {
ret = Some(action);
@@ -702,7 +714,6 @@ fn diff_col_ui(
if let Some(result) = symbol_list_ui(
ui,
SymbolDiffContext { obj, diff },
other_ctx.obj.map(|(obj, diff)| SymbolDiffContext { obj, diff }),
&state.symbol_state,
filter,
appearance,
@@ -764,3 +775,91 @@ fn find_symbol(obj: &Object, selected_symbol: &SymbolRefByName) -> Option<usize>
fn find_section(obj: &Object, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == section_name)
}
pub fn hover_items_ui(ui: &mut Ui, items: Vec<HoverItem>, appearance: &Appearance) {
for item in items {
match item {
HoverItem::Text { label, value, color } => {
let mut job = LayoutJob::default();
if !label.is_empty() {
let label_color = match color {
HoverItemColor::Special => appearance.replace_color,
_ => appearance.highlight_color,
};
write_text(&label, label_color, &mut job, appearance.code_font.clone());
write_text(": ", label_color, &mut job, appearance.code_font.clone());
}
write_text(
&value,
match color {
HoverItemColor::Emphasized => appearance.highlight_color,
_ => appearance.text_color,
},
&mut job,
appearance.code_font.clone(),
);
ui.label(job);
}
HoverItem::Separator => {
ui.separator();
}
}
}
}
pub fn context_menu_items_ui(
ui: &mut Ui,
items: Vec<ContextItem>,
column: usize,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
for item in items {
match item {
ContextItem::Copy { value, label } => {
let mut job = LayoutJob::default();
write_text(
"Copy \"",
appearance.text_color,
&mut job,
appearance.code_font.clone(),
);
write_text(
&value,
appearance.highlight_color,
&mut job,
appearance.code_font.clone(),
);
write_text("\"", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(label) = label {
write_text(" (", appearance.text_color, &mut job, appearance.code_font.clone());
write_text(
&label,
appearance.text_color,
&mut job,
appearance.code_font.clone(),
);
write_text(")", appearance.text_color, &mut job, appearance.code_font.clone());
}
if ui.button(job).clicked() {
ui.output_mut(|output| output.copied_text = value);
ui.close_menu();
}
}
ContextItem::Navigate { label, symbol_index, kind } => {
if ui.button(label).clicked() {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::new(
kind,
symbol_index,
column,
)));
ui.close_menu();
}
}
ContextItem::Separator => {
ui.separator();
}
}
}
ret
}
+38 -204
View File
@@ -4,17 +4,21 @@ use egui::{text::LayoutJob, Label, Response, Sense, Widget};
use egui_extras::TableRow;
use objdiff_core::{
diff::{
display::{display_row, DiffText, DiffTextColor, DiffTextSegment, HighlightKind},
display::{
display_row, instruction_context, instruction_hover, DiffText, DiffTextColor,
DiffTextSegment, HighlightKind,
},
DiffObjConfig, InstructionDiffKind, InstructionDiffRow, ObjectDiff,
},
obj::{
InstructionArg, InstructionArgValue, InstructionRef, Object, ParsedInstruction,
ResolvedRelocation, Section, Symbol,
},
obj::{InstructionArgValue, InstructionRef, Object},
util::ReallySigned,
};
use crate::views::{appearance::Appearance, symbol_diff::DiffViewAction};
use crate::views::{
appearance::Appearance,
diff::{context_menu_items_ui, hover_items_ui},
symbol_diff::DiffViewAction,
};
#[derive(Default)]
pub struct FunctionViewState {
@@ -67,51 +71,6 @@ impl FunctionViewState {
}
}
#[expect(unused)]
#[derive(Clone, Copy)]
pub struct ResolvedInstructionRef<'obj> {
pub symbol: &'obj Symbol,
pub section_idx: usize,
pub section: &'obj Section,
pub data: &'obj [u8],
pub relocation: Option<ResolvedRelocation<'obj>>,
}
fn resolve_instruction_ref(
obj: &Object,
symbol_idx: usize,
ins_ref: InstructionRef,
) -> Option<ResolvedInstructionRef> {
let symbol = &obj.symbols[symbol_idx];
let section_idx = symbol.section?;
let section = &obj.sections[section_idx];
let offset = ins_ref.address.checked_sub(section.address)?;
let data = section.data.get(offset as usize..offset as usize + ins_ref.size as usize)?;
let relocation = section.relocation_at(ins_ref, obj);
Some(ResolvedInstructionRef { symbol, section, section_idx, data, relocation })
}
fn resolve_instruction<'obj>(
obj: &'obj Object,
symbol_idx: usize,
ins_ref: InstructionRef,
diff_config: &DiffObjConfig,
) -> Option<(ResolvedInstructionRef<'obj>, ParsedInstruction)> {
let resolved = resolve_instruction_ref(obj, symbol_idx, ins_ref)?;
let ins = obj
.arch
.process_instruction(
ins_ref,
resolved.data,
resolved.relocation,
resolved.symbol.address..resolved.symbol.address + resolved.symbol.size,
resolved.section_idx,
diff_config,
)
.ok()?;
Some((resolved, ins))
}
fn ins_hover_ui(
ui: &mut egui::Ui,
obj: &Object,
@@ -120,86 +79,25 @@ fn ins_hover_ui(
diff_config: &DiffObjConfig,
appearance: &Appearance,
) {
let Some((
ResolvedInstructionRef { symbol, section_idx: _, section: _, data, relocation },
ins,
)) = resolve_instruction(obj, symbol_idx, ins_ref, diff_config)
else {
let Some(resolved) = obj.resolve_instruction_ref(symbol_idx, ins_ref) else {
ui.colored_label(appearance.delete_color, "Failed to resolve instruction");
return;
};
let ins = match obj.arch.process_instruction(resolved, diff_config) {
Ok(ins) => ins,
Err(e) => {
ui.colored_label(
appearance.delete_color,
format!("Failed to process instruction: {e}"),
);
return;
}
};
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.label(format!("{:02x?}", data));
if let Some(virtual_address) = symbol.virtual_address {
let offset = ins_ref.address - symbol.address;
ui.colored_label(
appearance.replace_color,
format!("Virtual address: {:#x}", virtual_address + offset),
);
}
// TODO
// if let Some(orig) = &ins.orig {
// ui.label(format!("Original: {}", orig));
// }
for arg in &ins.args {
if let InstructionArg::Value(arg) = arg {
match arg {
InstructionArgValue::Signed(v) => {
ui.label(format!("{arg} == {v}"));
}
InstructionArgValue::Unsigned(v) => {
ui.label(format!("{arg} == {v}"));
}
_ => {}
}
}
}
if let Some(resolved) = relocation {
ui.label(format!(
"Relocation type: {}",
obj.arch.display_reloc(resolved.relocation.flags)
));
let addend_str = match resolved.relocation.addend.cmp(&0i64) {
Ordering::Greater => format!("+{:x}", resolved.relocation.addend),
Ordering::Less => format!("-{:x}", -resolved.relocation.addend),
_ => "".to_string(),
};
ui.colored_label(
appearance.highlight_color,
format!("Name: {}{}", resolved.symbol.name, addend_str),
);
if let Some(orig_section_index) = resolved.symbol.section {
let section = &obj.sections[orig_section_index];
ui.colored_label(appearance.highlight_color, format!("Section: {}", section.name));
ui.colored_label(
appearance.highlight_color,
format!("Address: {:x}{}", resolved.symbol.address, addend_str),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", resolved.symbol.size),
);
// TODO
// for label in obj.arch.display_ins_data_labels(ins) {
// ui.colored_label(appearance.highlight_color, label);
// }
} else {
ui.colored_label(appearance.highlight_color, "Extern".to_string());
}
}
// TODO
// if let Some(decoded) = rlwinmdec::decode(&ins.formatted) {
// ui.colored_label(appearance.highlight_color, decoded.trim());
// }
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
hover_items_ui(ui, instruction_hover(obj, resolved, &ins), appearance);
});
}
@@ -208,93 +106,29 @@ fn ins_context_menu(
obj: &Object,
symbol_idx: usize,
ins_ref: InstructionRef,
column: usize,
diff_config: &DiffObjConfig,
appearance: &Appearance,
) {
let Some((
ResolvedInstructionRef { symbol, section_idx: _, section: _, data, relocation },
ins,
)) = resolve_instruction(obj, symbol_idx, ins_ref, diff_config)
else {
let Some(resolved) = obj.resolve_instruction_ref(symbol_idx, ins_ref) else {
ui.colored_label(appearance.delete_color, "Failed to resolve instruction");
return;
};
let ins = match obj.arch.process_instruction(resolved, diff_config) {
Ok(ins) => ins,
Err(e) => {
ui.colored_label(
appearance.delete_color,
format!("Failed to process instruction: {e}"),
);
return;
}
};
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
// TODO
// if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
// ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
// ui.close_menu();
// }
let mut hex_string = "0x".to_string();
for byte in data {
hex_string.push_str(&format!("{:02x}", byte));
}
if ui.button(format!("Copy \"{hex_string}\" (instruction bytes)")).clicked() {
ui.output_mut(|output| output.copied_text = hex_string);
ui.close_menu();
}
if let Some(virtual_address) = symbol.virtual_address {
let offset = ins_ref.address - symbol.address;
let offset_string = format!("{:#x}", virtual_address + offset);
if ui.button(format!("Copy \"{offset_string}\" (virtual address)")).clicked() {
ui.output_mut(|output| output.copied_text = offset_string);
ui.close_menu();
}
}
for arg in &ins.args {
if let InstructionArg::Value(arg) = arg {
match arg {
InstructionArgValue::Signed(v) => {
if ui.button(format!("Copy \"{arg}\"")).clicked() {
ui.output_mut(|output| output.copied_text = arg.to_string());
ui.close_menu();
}
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = v.to_string());
ui.close_menu();
}
}
InstructionArgValue::Unsigned(v) => {
if ui.button(format!("Copy \"{arg}\"")).clicked() {
ui.output_mut(|output| output.copied_text = arg.to_string());
ui.close_menu();
}
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = v.to_string());
ui.close_menu();
}
}
_ => {}
}
}
}
if let Some(resolved) = relocation {
// TODO
// for literal in obj.arch.display_ins_data_literals(ins) {
// if ui.button(format!("Copy \"{literal}\"")).clicked() {
// ui.output_mut(|output| output.copied_text.clone_from(&literal));
// ui.close_menu();
// }
// }
if let Some(name) = &resolved.symbol.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(name));
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", resolved.symbol.name)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&resolved.symbol.name));
ui.close_menu();
}
}
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
context_menu_items_ui(ui, instruction_context(obj, resolved, &ins), column, appearance);
});
}
@@ -410,7 +244,7 @@ pub(crate) fn asm_col_ui(
let response_cb = |response: Response| {
if let Some(ins_ref) = ins_row.ins_ref {
response.context_menu(|ui| {
ins_context_menu(ui, ctx.obj, symbol_ref, ins_ref, diff_config, appearance)
ins_context_menu(ui, ctx.obj, symbol_ref, ins_ref, column, diff_config, appearance)
});
response.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, ctx.obj, symbol_ref, ins_ref, diff_config, appearance)
+21 -11
View File
@@ -7,7 +7,6 @@ use std::{
use anyhow::Result;
use egui::{text::LayoutJob, Context, FontId, RichText, TextFormat, TextStyle, Window};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumMessage, IntoEnumIterator};
use crate::views::{appearance::Appearance, frame_history::FrameHistory};
@@ -20,23 +19,24 @@ pub struct GraphicsViewState {
pub should_relaunch: bool,
}
#[derive(
Copy, Clone, Debug, Default, PartialEq, Eq, EnumIter, EnumMessage, Serialize, Deserialize,
)]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum GraphicsBackend {
#[default]
#[strum(message = "Auto")]
Auto,
#[strum(message = "Vulkan")]
Vulkan,
#[strum(message = "Metal")]
Metal,
#[strum(message = "DirectX 12")]
Dx12,
#[strum(message = "OpenGL")]
OpenGL,
}
static ALL_BACKENDS: &[GraphicsBackend] = &[
GraphicsBackend::Auto,
GraphicsBackend::Vulkan,
GraphicsBackend::Metal,
GraphicsBackend::Dx12,
GraphicsBackend::OpenGL,
];
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct GraphicsConfig {
#[serde(default)]
@@ -70,6 +70,16 @@ impl GraphicsBackend {
GraphicsBackend::OpenGL => true,
}
}
pub fn display_name(self) -> &'static str {
match self {
GraphicsBackend::Auto => "Auto",
GraphicsBackend::Vulkan => "Vulkan",
GraphicsBackend::Metal => "Metal",
GraphicsBackend::Dx12 => "DirectX 12",
GraphicsBackend::OpenGL => "OpenGL",
}
}
}
pub fn graphics_window(
@@ -134,9 +144,9 @@ pub fn graphics_window(
ui.add_enabled_ui(state.graphics_config_path.is_some(), |ui| {
ui.horizontal(|ui| {
ui.label("Desired backend:");
for backend in GraphicsBackend::iter().filter(GraphicsBackend::is_supported) {
for backend in ALL_BACKENDS.iter().copied().filter(GraphicsBackend::is_supported) {
let selected = state.graphics_config.desired_backend == backend;
if ui.selectable_label(selected, backend.get_message().unwrap()).clicked() {
if ui.selectable_label(selected, backend.display_name()).clicked() {
let prev_backend = state.graphics_config.desired_backend;
state.graphics_config.desired_backend = backend;
match save_graphics_config(
File diff suppressed because it is too large Load Diff