Merge pull request #9094 from horizon-rust/dezer/modrinth

🤖 Agent: desktop fmt/build/clippy -D warnings/tests 0 at this head; Modrinth's Java mods stay listed for Horizon instances with the existing Works/Partly/Doesn't run mark, and installs still check SHA-512 before writing.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-04 12:43:20 +01:00
commit acd184c1d9
4 files changed
+106 -61

No files matched your search

+8 -6
View File
@@ -81,12 +81,14 @@ start); the hedge places and its recipe crafts 4.
## The Mods page
The launcher's **Mods** page has three tabs: **Mods** and **Shader packs** search Horizon's registry (a Java
instance's Mods tab searches Modrinth for its version), **Resource packs** searches Modrinth. Modrinth's Java mods and
data packs aren't usable yet, so the Mods tab has no source switch and Data packs has no tab; the CLI still reaches
them (`mods search --kind ...`). The Modpacks tab shows only while the sidebar's modpacks or its + button have it
open. Resource packs and Modpacks have a search field (Enter searches), **Show everything** and the source: Modrinth or
Horizon, with **CurseForge (coming soon)** beside it, disabled. Each result is an index row: its summary, author,
The launcher's **Mods** page has the five kinds as tabs: **Mods**, **Modpacks**, **Resource packs**, **Shader packs**
and **Data packs**, each with a search field (Enter searches). Mods, Modpacks and Resource packs pick their source:
Horizon's registry or Modrinth, with **CurseForge (coming soon)** beside them, disabled. Mods start on Horizon's
registry; **Shader packs** search it only (Horizon's own `.hshader` packs), **Data packs** search Modrinth only and
install into the world picked under the search. A Java instance's tabs always search Modrinth for its version and
loader, with the version and loader filters. A Horizon instance searching Modrinth's Java mods sees them all, each
marked **Works**, **Partly** or **Doesn't run in Horizon** once its jar is checked; **Show everything** adds what was
made for other Minecraft versions. Each result is an index row: its summary, author,
downloads, source and licence, how well it runs (and why), **Page** (the project's page on its source) and
**Install** (**Import** for a modpack). The Modpacks tab lists the imported packs first, with their coverage
report, **Port** and **Play** ([modpacks.md](modpacks.md)). Installed packs are listed above the results with their version, source,
+10 -7
View File
@@ -42,10 +42,13 @@ The opening, then Play ([clip](docs/opening.webm)):
starts the game. With no install on the computer the Play page offers the download alone and the
Minecraft folder row says none is needed. Without an owner,
the Play page asks to sign in first. `horizon-launcher prepare --download`.
- **Mods**: **Mods** and **Shader packs** search Horizon's own registry (a Java instance's mods
come from Modrinth), **Resource packs** searches Modrinth, each marked Works, Partly or Doesn't
run in Horizon; Modrinth's data packs aren't usable yet and have no tab. Install puts a file in the game folder only after its checksum matches, and a
modpack (the sidebar's +) goes to the Modpacks page; installed packs show their source and checksum, with Update,
- **Mods**: tabs for Mods, Modpacks, Resource packs, Shader packs and Data packs. Mods, Modpacks
and Resource packs search Horizon's own registry or Modrinth (CurseForge support is coming soon);
Shader packs search the registry, Data packs Modrinth (into the world picked there). A Java
instance's files come from Modrinth for its version and loader; on a Horizon instance, Modrinth's
Java mods are listed too, each marked Works, Partly or Doesn't run in Horizon. Install puts a file in
the game folder only after its SHA-512 matches, and a
modpack goes to the Modpacks page; installed packs show their source and checksum, with Update,
**Update all** and Remove ([docs/mod-browser.md](../docs/mod-browser.md)).
- **Modpacks**: import a Java modpack (a Modrinth `.mrpack`, a CurseForge zip, or a Prism,
MultiMC or CurseForge instance folder) and see its coverage report: per mod, ported, partial,
@@ -190,9 +193,9 @@ The opening, then Play ([clip](docs/opening.webm)):
`datapacks/` under its row: what is there (and which the game has turned off), **Add data pack…**,
and **Remove** (to the trash); a data pack `.zip` dropped on a world goes into it. The game turns
a new one on when the world opens. From the command line: `horizon-launcher worlds
datapack-list|datapack-add|datapack-remove <world> …`. `horizon-launcher mods search --kind datapack`
finds them on Modrinth and `mods install <pack> --world <world>` installs one, saying what of it
runs in Horizon.
datapack-list|datapack-add|datapack-remove <world> …`. The Mods page's **Data packs**
tab finds them on Modrinth and installs one into the world picked there (`mods search --kind
datapack`, `mods install <pack> --world <world>`), saying what of it runs in Horizon.
- **Packs**: Home lists the selected instance's resource packs and shader packs, each with a switch.
The choice is the game's own: `resourcePacks` in options.txt (a pack turned on goes on top, as
the game's Resource Packs screen does) and `pickedShaderPack` in `horizon-graphics.txt` (one shader
+55 -26
View File
@@ -6,10 +6,14 @@
//! instance).
//!
//! The mockup's tabs are its sources (Horizon, Modrinth, CurseForge soon); the launcher's page
//! is by kind (mods and shader packs from Horizon's registry, resource packs and modpacks from
//! Modrinth), each kind with its sources, so the tabs are the kinds, CurseForge after them as
//! coming soon. Not in the mockup, kept in its patterns: Show everything, the source switch, the
//! filters and the sort, which instance or world installs go to (under the search); the
//! is by kind (mods, modpacks, resource packs, shader packs, data packs), each with its sources,
//! so the tabs are the kinds and the sources are a switch under the search: Horizon's registry
//! or Modrinth, CurseForge beside them as coming soon, on the kinds both serve (`switches`):
//! shader packs are Horizon's registry's only, data packs Modrinth's only, and a Java
//! instance's files always Modrinth's. Modrinth's Java mods are offered to a Horizon instance
//! too, each marked Works, Partly or Doesn't run in Horizon. Not in the mockup, kept in its
//! patterns: Show everything, the filters, which instance or world installs go to (under the
//! search); the
//! project's page, its rating and why it runs or not (in the project's card); adding a native mod
//! from GitHub, the instance's installed list and its native build, imported modpacks and the
//! install's progress (cards under the two).
@@ -36,7 +40,9 @@ impl Launcher {
&[("count", &m.results.len().to_string())],
)
.into()
} else if m.source == SourceId::Modrinth {
} else if m.source == SourceId::Modrinth
&& matches!(m.kind, PackKind::ResourcePack | PackKind::Modpack)
{
tr("Search resource packs and modpacks").into()
} else {
tr("Search, then Enter").into()
@@ -59,10 +65,7 @@ impl Launcher {
.mb_0();
let open = self.mods.kind;
let mut tabs = div().flex().flex_none().items_center().gap(px(4.));
for (kind, label, _) in KINDS
.into_iter()
.filter(|&(kind, ..)| SHOWN.contains(&kind) || kind == open)
{
for (kind, label, _) in KINDS {
tabs = tabs.child(
small_tab(
SharedString::from(format!("mods-tab-{label}")),
@@ -76,19 +79,6 @@ impl Launcher {
})),
);
}
// CurseForge: shown, inert, never searched, as in the mockup.
tabs = tabs.child(
div()
.flex()
.items_center()
.gap(px(6.))
.h(px(32.))
.px(px(12.))
.text_size(px(13.))
.text_color(white(0.35))
.child("CurseForge")
.child(tr("soon").to_string()),
);
let header = div()
.flex()
.items_end()
@@ -141,10 +131,9 @@ impl Launcher {
})),
)
})
.when(
source_for(self.mods.kind) == SourceId::Modrinth && !java,
|d| d.child(div().flex().child(self.mods_source_switch(cx))),
)
.when(switches(self.mods.kind) && !java, |d| {
d.child(self.glass_source_switch(cx))
})
.children((!registry).then(|| self.mods_filters(cx)))
.children(self.mods_target_line(cx));
// The search, and the sort on its right.
@@ -204,6 +193,46 @@ impl Launcher {
)
}
/// The source switch (`switches`): Horizon's registry or Modrinth as small tabs, CurseForge
/// after them as coming soon, shown, inert, never searched, as in the mockup.
fn glass_source_switch(&mut self, cx: &mut Context<Self>) -> Div {
let t = self.theme;
let current = self.mods.source;
let mut row = div().flex().items_center().gap(px(4.));
for (source, label) in SOURCES {
row = row.child(
small_tab(
SharedString::from(format!("mods-source-{label}")),
source == current,
&t,
)
.child(tr(label).to_string())
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
if this.mods.source != source {
this.mods.source = source;
this.mods.browse.filters = Default::default();
this.mods.browse.open = None;
this.mods.picked = None;
this.mods_search(0);
}
cx.notify();
})),
);
}
row.child(
div()
.flex()
.items_center()
.gap(px(6.))
.h(px(32.))
.px(px(12.))
.text_size(px(13.))
.text_color(white(0.35))
.child("CurseForge")
.child(tr("soon").to_string()),
)
}
/// The "Sort by" dropdown beside the search: as tall and as round as the search, white at
/// 7 % like it; the choices on a glass popover under it, the chosen one ticked.
fn glass_mods_sort(&mut self, cx: &mut Context<Self>) -> Div {
+33 -22
View File
@@ -1,5 +1,7 @@
//! The Mods page: search Horizon's own registry for mods and shader packs made for Horizon, or
//! Modrinth for resource packs (a Java instance's mods too), see where each comes
//! The Mods page: its tabs are the kinds (mods, modpacks, resource packs, shader packs, data
//! packs). Search Horizon's own registry for what was made for Horizon, or Modrinth (CurseForge:
//! coming soon) for Java mods, modpacks, resource packs and data packs (a Java instance's files
//! always come from Modrinth, for its version), see where each comes
//! from and how well it runs (a Java mod's jar is checked first; a Horizon mod says whether it is
//! signed and what it may do), install it with its checksum checked, turn Horizon mods on and
//! off, and update or remove what was installed (`horizon_launcher::browser`). Imported modpacks
@@ -17,7 +19,7 @@ use java_instance::Target;
use std::collections::HashMap;
use std::path::PathBuf;
/// The kinds, as the Mods page's tabs: name and icon. Only [`SHOWN`]'s have a tab.
/// The kinds, as the Mods page's tabs: name and icon.
const KINDS: [(PackKind, &str, &str); 5] = [
(PackKind::Mod, "Mods", "box"),
(PackKind::Modpack, "Modpacks", "archive"),
@@ -25,15 +27,13 @@ const KINDS: [(PackKind, &str, &str); 5] = [
(PackKind::Shader, "Shader packs", "zap"),
(PackKind::DataPack, "Data packs", "notes"),
];
/// The kinds with a tab: Horizon's registry's, and Modrinth's resource packs. Modrinth's mods and
/// data packs aren't usable yet; the Modpacks tab shows while the sidebar has it open.
const SHOWN: [PackKind; 3] = [PackKind::Mod, PackKind::ResourcePack, PackKind::Shader];
/// Java mods checked at once.
const CHECKING: usize = 2;
/// The sources to pick from on the Resource packs and Modpacks tabs; CurseForge is shown beside them as coming soon.
const SOURCES: [(SourceId, &str); 2] = [
(SourceId::Modrinth, "Modrinth"),
/// The sources to pick from on the tabs that have both ([`switches`]); CurseForge is shown
/// beside them as coming soon.
pub(super) const SOURCES: [(SourceId, &str); 2] = [
(SourceId::Horizon, "Horizon"),
(SourceId::Modrinth, "Modrinth"),
];
const PAGE: usize = 20;
@@ -159,6 +159,7 @@ impl Mods {
Ok("modpacks") => PackKind::Modpack,
Ok("resourcepacks") => PackKind::ResourcePack,
Ok("shaders") => PackKind::Shader,
Ok("datapacks") => PackKind::DataPack,
_ => PackKind::Mod,
};
Self {
@@ -940,6 +941,10 @@ impl Launcher {
if self.mods.kind != kind {
self.mods.kind = kind;
self.mods.source = source_for(kind);
// A data pack goes into a world: one may have been made since.
if kind == PackKind::DataPack {
self.mods.read_worlds();
}
// Categories and loaders differ by kind.
self.mods.browse.filters = Default::default();
self.mods.browse.open = None;
@@ -1021,11 +1026,7 @@ impl Launcher {
);
let this = cx.entity().downgrade();
let open = self.mods.kind;
let shown: Vec<_> = KINDS
.into_iter()
.filter(|&(kind, ..)| SHOWN.contains(&kind) || kind == open)
.collect();
let kinds = blocks::tabs("mods-kind", &shown, open, &t, {
let kinds = blocks::tabs("mods-kind", &KINDS, open, &t, {
let this = this.clone();
move |kind, _, cx| {
let _ = this.update(cx, |this, cx| {
@@ -1052,11 +1053,11 @@ impl Launcher {
this.mods_search(0);
cx.notify();
}));
// Only the kinds Modrinth serves pick their source: CurseForge is coming soon there,
// shown, inert, never searched (like the Microsoft account's "Coming soon").
// A Java instance's mods and resource packs come from Modrinth, for its version.
// The kinds both sources serve pick theirs: CurseForge is coming soon there, shown,
// inert, never searched (like the Microsoft account's "Coming soon").
// A Java instance's files come from Modrinth, for its version.
let java = self.mods.java_for(self.mods.kind).is_some();
let source_note = (source_for(self.mods.kind) == SourceId::Modrinth && !java)
let source_note = (switches(self.mods.kind) && !java)
.then(|| div().flex().child(self.mods_source_switch(cx)));
// The registry lists only what was made for Horizon: no "everything", no Modrinth filters,
// only its sort.
@@ -1838,15 +1839,25 @@ impl Launcher {
}
}
/// Where a kind's tab searches first: Horizon's registry, but Modrinth for resource packs and
/// modpacks.
/// Where a kind's tab searches first: Horizon's registry for mods and shader packs, Modrinth for
/// the rest (data packs are only there).
fn source_for(kind: PackKind) -> SourceId {
match kind {
PackKind::ResourcePack | PackKind::Modpack => SourceId::Modrinth,
_ => SourceId::Horizon,
PackKind::Mod | PackKind::Shader => SourceId::Horizon,
PackKind::ResourcePack | PackKind::Modpack | PackKind::DataPack => SourceId::Modrinth,
}
}
/// Whether a kind's tab picks its source: mods, modpacks and resource packs are on both Horizon's
/// registry and Modrinth; Horizon's shader packs are on its registry only, data packs on Modrinth
/// only.
pub(super) fn switches(kind: PackKind) -> bool {
matches!(
kind,
PackKind::Mod | PackKind::ResourcePack | PackKind::Modpack
)
}
impl Launcher {
/// The source switch: Modrinth or Horizon's registry, CurseForge shown as coming soon and not
/// clickable, like the Microsoft account's "Coming soon".