ltk/
resources.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! An app's own copy of the resources ltk otherwise finds installed on the system: for an Android package, or any bundle with nowhere to install them.
//!
//! The directory holds `themes/<id>/`, searched after the user's themes and before the system's, and `fonts/`. A font a theme names by an absolute path that does not exist is looked up in `fonts/` by its file name, so a theme written against Debian's font paths works unchanged from a bundle that carries those files.

use std::io;
use std::path::{ Path, PathBuf };
use std::sync::OnceLock;

static RESOURCE_DIR: OnceLock<PathBuf> = OnceLock::new();

/// Use `dir` as the app's resource bundle. Call it before the theme is first used; later calls are ignored.
pub fn set_resource_dir( dir: impl Into<PathBuf> )
{
	let _ = RESOURCE_DIR.set( dir.into() );
}

/// The app's resource bundle, when [`set_resource_dir`] gave one; on Android, the resources unpacked from the APK; on Windows, otherwise, `ltk\` beside the executable when it exists. An app finds its own bundled files there.
pub fn resource_dir() -> Option<&'static Path>
{
	RESOURCE_DIR.get().map( PathBuf::as_path ).or_else( beside_executable )
}

#[ cfg( windows ) ]
fn beside_executable() -> Option<&'static Path>
{
	static DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
	DIR.get_or_init( ||
	{
		let dir = std::env::current_exe().ok()?.parent()?.join( "ltk" );
		dir.is_dir().then_some( dir )
	} ).as_deref()
}

#[ cfg( not( windows ) ) ]
fn beside_executable() -> Option<&'static Path>
{
	None
}

/// The font at `path`, or when there is none, the bundled font of the same file name.
pub( crate ) fn read_font( path: &Path ) -> io::Result<Vec<u8>>
{
	read_font_in( path, resource_dir() )
}

fn read_font_in( path: &Path, bundle: Option<&Path> ) -> io::Result<Vec<u8>>
{
	match crate::vfs::read( path )
	{
		Err( e ) if e.kind() == io::ErrorKind::NotFound =>
		{
			let Some( ( dir, name ) ) = bundle.zip( path.file_name() ) else { return Err( e ) };
			crate::vfs::read( dir.join( "fonts" ).join( name ) )
		}
		read => read,
	}
}

#[ cfg( test ) ]
mod tests
{
	use super::read_font_in;
	use std::path::Path;

	#[ test ]
	fn a_missing_font_is_found_in_the_bundle_by_name()
	{
		let bundle = std::env::temp_dir().join( format!( "ltk-resources-{}", std::process::id() ) );
		std::fs::create_dir_all( bundle.join( "fonts" ) ).unwrap();
		std::fs::write( bundle.join( "fonts/Face-Regular.otf" ), b"face" ).unwrap();
		let read = read_font_in( Path::new( "/nonexistent/fonts/Face-Regular.otf" ), Some( &bundle ) );
		std::fs::remove_dir_all( &bundle ).unwrap();
		assert_eq!( read.unwrap(), b"face" );
	}

	#[ test ]
	fn without_a_bundle_a_missing_font_stays_missing()
	{
		assert!( read_font_in( Path::new( "/nonexistent/fonts/Face-Regular.otf" ), None ).is_err() );
	}
}