feat: added memory allocation presets to minecraft and cluster settings (#747)

This commit is contained in:
Maciek 2026-08-26 12:53:48 +02:00 • committed by GitHub
parent 8ca2c30993
commit 419c07b822
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 154 additions and 34 deletions

View file

@ -0,0 +1,51 @@
use freya::prelude::*;
use crate::components::{Dropdown, TextInput, validate_memory};
use crate::theme::colors;
use crate::utils::{format_memory_gb, memory_presets_mb};
const UNSET_LABEL: &str = "Default";
const CUSTOM_LABEL: &str = "Custom";
/// Presets picker with an input for memory allocation
pub fn memory_field(mut memory: State<String>) -> impl IntoElement {
let presets = memory_presets_mb();
let selected = match memory.read().trim() {
"" => UNSET_LABEL.to_string(),
value => value
.parse::<u32>()
.ok()
.filter(|mb| presets.contains(mb))
.map(format_memory_gb)
.unwrap_or_else(|| CUSTOM_LABEL.to_string()),
};
let options: Vec<String> = presets.iter().copied().map(format_memory_gb).collect();
rect()
.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
.child(
Dropdown::new(selected, options)
.width(Size::px(100.))
.height(Size::px(34.))
.on_select(move |idx: usize| {
if let Some(mb) = presets.get(idx).copied() {
memory.set(mb.to_string());
}
}),
)
.child(
TextInput::new(memory)
.width(Size::px(90.))
.placeholder("4096")
.on_validate(validate_memory)
.trailing(
label()
.text("MB")
.font_size(12.)
.color(colors::fg_secondary()),
),
)
}

View file

@ -16,6 +16,7 @@ mod java_prompt;
mod link_confirm;
mod local_image;
mod log_viewer;
mod memory_field;
mod microsoft_login;
mod navbar;
mod notifications;
@ -57,6 +58,7 @@ pub use java_prompt::JavaPromptOverlay;
pub use link_confirm::ConfirmLinkOverlay;
pub use local_image::LocalImage;
pub use log_viewer::LogViewer;
pub use memory_field::memory_field;
pub use microsoft_login::use_microsoft_login;
pub(crate) use microsoft_login::login_dialog;
pub(crate) use navbar::window_controls;
@ -76,7 +78,7 @@ pub use segmented_control::{Segment, SegmentedControl};
pub use splash_curtain::SplashCurtain;
pub use status_bar::StatusBar;
pub use tab_bar::{TabBar, TabItem};
pub use text_input::{TextInput, validate_number};
pub use text_input::{TextInput, validate_memory, validate_number};
pub use toasts::Toasts;
pub use toggle::{toggle, toggle_controlled};
pub use update_prompt::UpdatePromptOverlay;

View file

@ -25,6 +25,18 @@ pub fn validate_number(validator: InputValidator) {
validator.set_valid(valid);
}
/// Validates the input if user has entered more MB of memory than his machine has
pub fn validate_memory(validator: InputValidator) {
let text = validator.text();
let valid = text.is_empty()
|| text
.parse::<u32>()
.is_ok_and(|mb| mb <= crate::utils::total_ram_mb());
validator.set_valid(valid);
}
#[derive(Clone, PartialEq)]
pub struct TextInput {
input: Input,

View file

@ -5,6 +5,7 @@ use chrono::{Datelike, NaiveDate};
use oneclient_core::clusters::Cluster;
use oneclient_common::domain::GameLoader;
use oneclient_common::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_version};
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
pub type ClusterGroups = BTreeMap<ReleaseLine, Vec<Cluster>>;
@ -204,6 +205,56 @@ pub fn format_res((w, h): (u32, u32)) -> String {
format!("{w}×{h}")
}
/// Prevents user from choosing his max amount of ram preset (e.g Someone has 16GB of RAM,
/// so the max preset is 16GB - 2GB = 14GB)
const MEMORY_HEADROOM_GB: u32 = 2;
const MEMORY_PRESETS_GB: [u32; 10] = [2, 4, 6, 8, 12, 16, 24, 32, 48, 64];
pub fn total_ram_mb() -> u32 {
static TOTAL_RAM_MB: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
*TOTAL_RAM_MB.get_or_init(|| {
let system = System::new_with_specifics(
RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()),
);
(system.total_memory() / 1024 / 1024).min(u32::MAX as u64) as u32
})
}
pub fn memory_presets_mb() -> Vec<u32> {
presets_for_total_gb((total_ram_mb() as f32 / 1024.).round() as u32)
}
fn presets_for_total_gb(total_gb: u32) -> Vec<u32> {
let usable_gb = total_gb.saturating_sub(MEMORY_HEADROOM_GB);
if usable_gb == 0 {
return vec![1024];
}
let mut presets: Vec<u32> = MEMORY_PRESETS_GB
.iter()
.copied()
.filter(|gb| *gb <= usable_gb)
.map(|gb| gb * 1024)
.collect();
let usable_mb = usable_gb * 1024;
if presets.last() != Some(&usable_mb) {
presets.push(usable_mb);
}
presets
}
/// `8192` -> `8 GB` `1536` -> `1.5 GB`
pub fn format_memory_gb(mb: u32) -> String {
if mb.is_multiple_of(1024) {
format!("{} GB", mb / 1024)
} else {
format!("{:.1} GB", mb as f32 / 1024.)
}
}
/// `7384` -> `2h 3m` `540` -> `9m` `0` -> `0m`
pub fn format_duration_hm(secs: i64) -> String {
if secs <= 0 {
@ -396,6 +447,35 @@ mod tests {
assert_eq!(line_art_key(line(26, Some(1)), &modern), Some((1, None)));
}
#[test]
fn presets_stop_two_gigabytes_short_of_the_machine() {
assert_eq!(
presets_for_total_gb(16),
vec![2048, 4096, 6144, 8192, 12288, 14336]
);
assert_eq!(
presets_for_total_gb(32),
vec![2048, 4096, 6144, 8192, 12288, 16384, 24576, 30720]
);
}
#[test]
fn a_ceiling_landing_on_a_round_step_is_not_repeated() {
assert_eq!(presets_for_total_gb(8), vec![2048, 4096, 6144]);
}
#[test]
fn a_tiny_machine_still_gets_one_preset() {
assert_eq!(presets_for_total_gb(4), vec![2048]);
assert_eq!(presets_for_total_gb(2), vec![1024]);
}
#[test]
fn memory_labels_drop_the_decimal_when_whole() {
assert_eq!(format_memory_gb(8192), "8 GB");
assert_eq!(format_memory_gb(1536), "1.5 GB");
}
#[test]
fn a_bare_legacy_version_keeps_its_line_name() {
let clusters = [versioned(1, "1.21")];

View file

@ -7,8 +7,8 @@ use oneclient_core::settings::{
};
use crate::components::{
Button, Dropdown, Icon, IconType, ScrollArea, TextInput, toggle, toggle_controlled,
validate_number,
Button, Dropdown, Icon, IconType, ScrollArea, TextInput, memory_field, toggle,
toggle_controlled, validate_number,
};
use crate::hooks::{
ClusterAction, java_runtimes, loader_versions, mutation_is_running, query_error,
@ -363,21 +363,12 @@ impl Component for MemoryRow {
})
.into();
let control = TextInput::new(memory)
.width(Size::px(90.))
.placeholder("4096")
.on_validate(validate_number)
.trailing(
label()
.text("MB")
.font_size(12.)
.color(colors::fg_secondary()),
);
let control = memory_field(memory);
settings_row(
IconType::Database01,
"Memory",
"The amount of memory in megabytes allocated for the game.",
"The amount of memory in megabytes allocated for the game. Presets leave 2 GB for the system.",
override_cell(control, overridden, on_reset),
)
}

View file

@ -3,7 +3,9 @@ use oneclient_common::Patch;
use oneclient_core::settings::{PackageUpdateMode, ProfileUpdate, Resolution};
use super::settings_page;
use crate::components::{Dropdown, Icon, IconType, TextInput, toggle, validate_number};
use crate::components::{
Dropdown, Icon, IconType, TextInput, memory_field, toggle, validate_number,
};
use crate::hooks::{use_dispatch, use_settings_snapshot};
use crate::theme::colors;
use crate::view::app::settings::{section_header, settings_row};
@ -92,7 +94,7 @@ impl Component for SettingsMinecraft {
.child(settings_row(
IconType::Database01,
"Memory",
"The amount of memory in megabytes allocated for the game.",
"The amount of memory in megabytes allocated for the game. Presets leave 2 GB for the system.",
memory_field(memory),
))
.child(settings_row(
@ -237,21 +239,3 @@ fn resolution_field(width: State<String>, height: State<String>) -> impl IntoEle
.into_element()
}
fn memory_field(memory: State<String>) -> impl IntoElement {
rect()
.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
.child(
TextInput::new(memory)
.width(Size::px(90.))
.placeholder("4096")
.on_validate(validate_number)
.trailing(
label()
.text("MB")
.font_size(12.)
.color(colors::fg_secondary()),
),
)
}