Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/oneclient_app/src/components/memory_field.rs
Original file line number Diff line number Diff line change
@@ -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()),
),
)
}
4 changes: 3 additions & 1 deletion packages/oneclient_app/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,6 +56,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;
Expand All @@ -73,7 +75,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, toggle_disabled};
pub use update_prompt::UpdatePromptOverlay;
Expand Down
12 changes: 12 additions & 0 deletions packages/oneclient_app/src/components/text_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
80 changes: 80 additions & 0 deletions packages/oneclient_app/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")];
Expand Down
17 changes: 4 additions & 13 deletions packages/oneclient_app/src/view/app/cluster/cluster_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, try_game_profile,
Expand Down Expand Up @@ -355,21 +355,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),
)
}
Expand Down
24 changes: 4 additions & 20 deletions packages/oneclient_app/src/view/app/settings/minecraft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()),
),
)
}