///|
const RAYLIB_DEFAULT_TEXT_SPACING : Float = 1.0
///|
const RAYLIB_DEFAULT_AUDIO_STREAM_BUFFER_SIZE = 4096
///|
fn reset_cosmic_runtime() -> Unit {
backend.cosmic_loaded_families.clear()
for _key, entry in backend.cosmic_textures {
@raylib.unload_texture(entry.texture)
}
backend.cosmic_textures.clear()
cosmic_font_system.val = @cosmic.FontSystem::new()
cosmic_swash_cache.val = @cosmic.SwashCache::new()
}
///|
fn ensure_cosmic_font_loaded(family : String) -> Bool {
if backend.cosmic_loaded_families.contains(family) {
return true
}
guard backend.font_alias.get(family) is Some(path) else { return false }
guard get_asset_bytes(path) is Some(bytes) else { return false }
cosmic_font_system.val = cosmic_font_system.val.load_font_data(bytes)
backend.cosmic_loaded_families.add(family)
true
}
///|
fn cosmic_text_cache_key(command : @render2d_types.TextDrawCommand2D) -> String {
let text = command.text
let style = command.style
let color = style.color
let alpha = clamp_u8((color.a * 255.0).round().to_int())
let align = match style.align {
Left => "left"
Center => "center"
Right => "right"
}
let mut key = "t:" +
text.length().to_string() +
":" +
text +
"|f:" +
style.family.length().to_string() +
":" +
style.family +
"|s:" +
style.size.to_string() +
"|w:" +
style.weight.value().to_string() +
"|rs:" +
backend.text_raster_scale.to_string() +
"|r:" +
color.r.to_string() +
"|g:" +
color.g.to_string() +
"|b:" +
color.b.to_string() +
"|a:" +
alpha.to_string() +
"|lh:" +
style.line_height.to_string() +
"|ls:" +
style.letter_spacing.to_string() +
"|mw:" +
command.max_width.map_or("none", fn(value) { value.to_string() }) +
"|mh:" +
command.max_height.map_or("none", fn(value) { value.to_string() }) +
"|wrap:" +
command.wrap.to_string() +
"|ellipsis:" +
command.ellipsis.to_string() +
"|align:" +
align
for section in command.sections {
let section_color = section.style.color
key = key +
"|section:" +
section.text.length().to_string() +
":" +
section.text +
":" +
section.style.family +
":" +
section.style.size.to_string() +
":" +
section.style.weight.value().to_string() +
":" +
section.style.line_height.to_string() +
":" +
section.style.letter_spacing.to_string() +
":" +
section_color.r.to_string() +
":" +
section_color.g.to_string() +
":" +
section_color.b.to_string() +
":" +
section_color.a.to_string() +
":" +
section.underline.to_string()
}
key
}
///|
fn cosmic_font_size(size : Double) -> Double {
if size <= 0.0 {
1.0
} else {
size
}
}
///|
fn rasterize_with_cosmic(
command : @render2d_types.TextDrawCommand2D,
) -> CosmicRasterImage? {
guard command.text != "" || !command.sections.is_empty() else { return None }
guard ensure_cosmic_font_loaded(command.style.family) else { return None }
for section in command.sections {
guard ensure_cosmic_font_loaded(section.style.family) else { return None }
}
let buffer = @text_pipeline.compute_text_block(
command,
cosmic_font_system.val,
backend.text_raster_scale,
).buffer()
let mut min_x = 0
let mut min_y = 0
let mut max_x = -1
let mut max_y = -1
let fills : Array[(Int, Int, Int, Int, Int, Int, Int, Int)] = []
let (drawn_buffer, swash_cache) = buffer.draw(
cosmic_font_system.val,
cosmic_swash_cache.val,
@cosmic.Color::rgba(
command.style.color.r.to_byte(),
command.style.color.g.to_byte(),
command.style.color.b.to_byte(),
clamp_u8((command.style.color.a * 255.0).round().to_int()).to_byte(),
),
fn(x, y, width, height, color) {
let w = width.reinterpret_as_int()
let h = height.reinterpret_as_int()
guard w > 0 && h > 0 else { return }
let (r, g, b, a) = color.as_rgba()
fills.push((x, y, w, h, r.to_int(), g.to_int(), b.to_int(), a.to_int()))
let right = x + w - 1
let bottom = y + h - 1
if max_x < min_x {
min_x = x
min_y = y
max_x = right
max_y = bottom
} else {
if x < min_x {
min_x = x
}
if y < min_y {
min_y = y
}
if right > max_x {
max_x = right
}
if bottom > max_y {
max_y = bottom
}
}
},
)
ignore(drawn_buffer)
cosmic_swash_cache.val = swash_cache
guard !fills.is_empty() else { return None }
let width = max_x - min_x + 1
let height = max_y - min_y + 1
let pixels = Array::make(width * height * 4, 0)
for fill in fills {
let x = fill.0
let y = fill.1
let w = fill.2
let h = fill.3
let r = clamp_u8(fill.4)
let g = clamp_u8(fill.5)
let b = clamp_u8(fill.6)
let a = clamp_u8(fill.7)
for local_y in 0..= height {
continue
}
for local_x in 0..= width {
continue
}
let idx = (py * width + px) * 4
pixels[idx] = r
pixels[idx + 1] = g
pixels[idx + 2] = b
pixels[idx + 3] = a
}
}
}
Some({ width, height, pixels })
}
///|
fn texture_from_cosmic_raster(raster : CosmicRasterImage) -> CosmicTextTexture? {
guard raster.width > 0 && raster.height > 0 else { return None }
let image = @raylib.gen_image_color(
raster.width,
raster.height,
@raylib.Color::new(0, 0, 0, 0),
)
let texture = @raylib.load_texture_from_image(image)
@raylib.unload_image(image)
@raylib.update_texture(
texture,
Bytes::from_iter(
raster.pixels.iter().map(fn(value) { clamp_u8(value).to_byte() }),
),
)
@raylib.set_texture_filter(texture, configured_filter(sampler=Linear))
Some({
texture,
width: raster.width,
height: raster.height,
raster_scale: backend.text_raster_scale,
})
}
///|
fn cosmic_texture_for_text(
command : @render2d_types.TextDrawCommand2D,
) -> CosmicTextTexture? {
let key = cosmic_text_cache_key(command)
if backend.cosmic_textures.get(key) is Some(cached) {
return Some(cached)
}
guard rasterize_with_cosmic(command) is Some(raster) else { return None }
guard texture_from_cosmic_raster(raster) is Some(created) else { return None }
backend.cosmic_textures.set(key, created)
Some(created)
}
///|
fn measure_text_with_cosmic(
command : @render2d_types.TextDrawCommand2D,
) -> @smath.Vec2? {
guard ensure_cosmic_font_loaded(command.style.family) else { return None }
for section in command.sections {
guard ensure_cosmic_font_loaded(section.style.family) else { return None }
}
Some(
@text_pipeline.compute_text_block(command, cosmic_font_system.val, 1.0).size(),
)
}
///|
fn draw_text_with_cosmic(command : @render2d_types.TextDrawCommand2D) -> Bool {
guard command.text != "" || !command.sections.is_empty() else { return true }
guard cosmic_texture_for_text(command) is Some(cached_texture) else {
return false
}
let measured = measure_text_with_cosmic(command).unwrap_or(
Vec2(
cached_texture.width.to_double() / cached_texture.raster_scale,
cached_texture.height.to_double() / cached_texture.raster_scale,
),
)
let mut x = command.position[X]
let mut y = command.position[Y]
match command.style.align {
Left => ()
Center => x -= measured[X] / 2.0
Right => x -= measured[X]
}
match command.style.baseline {
Top => ()
Center => y -= measured[Y] / 2.0
Bottom => y -= measured[Y]
}
@raylib.draw_texture_pro(
cached_texture.texture,
to_ray_rect(
0.0,
0.0,
cached_texture.width.to_double(),
cached_texture.height.to_double(),
),
to_ray_rect(
x,
y,
cached_texture.width.to_double() / cached_texture.raster_scale,
cached_texture.height.to_double() / cached_texture.raster_scale,
),
to_ray_vector2(0.0, 0.0),
0.0,
to_ray_color(white_render_color()),
)
true
}
///|
fn key_to_raylib(code : @inputs.Code) -> Int {
match code {
KeyA => @raylib.KeyA
KeyB => @raylib.KeyB
KeyC => @raylib.KeyC
KeyD => @raylib.KeyD
KeyE => @raylib.KeyE
KeyF => @raylib.KeyF
KeyG => @raylib.KeyG
KeyH => @raylib.KeyH
KeyI => @raylib.KeyI
KeyJ => @raylib.KeyJ
KeyK => @raylib.KeyK
KeyL => @raylib.KeyL
KeyM => @raylib.KeyM
KeyN => @raylib.KeyN
KeyO => @raylib.KeyO
KeyP => @raylib.KeyP
KeyQ => @raylib.KeyQ
KeyR => @raylib.KeyR
KeyS => @raylib.KeyS
KeyT => @raylib.KeyT
KeyU => @raylib.KeyU
KeyV => @raylib.KeyV
KeyW => @raylib.KeyW
KeyX => @raylib.KeyX
KeyY => @raylib.KeyY
KeyZ => @raylib.KeyZ
ArrowUp => @raylib.KeyUp
ArrowDown => @raylib.KeyDown
ArrowLeft => @raylib.KeyLeft
ArrowRight => @raylib.KeyRight
Tab => @raylib.KeyTab
ShiftLeft => @raylib.KeyLeftShift
Space => @raylib.KeySpace
Enter => @raylib.KeyEnter
Escape => @raylib.KeyEscape
}
}
///|
fn all_codes() -> Array[@inputs.Code] {
[
KeyA,
KeyB,
KeyC,
KeyD,
KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL,
KeyM,
KeyN,
KeyO,
KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV,
KeyW,
KeyX,
KeyY,
KeyZ,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Tab,
ShiftLeft,
Space,
Enter,
Escape,
]
}
///|
fn sync_key_events() -> Unit {
if backend.key_events is Some(pressed_keys) {
for code in all_codes() {
if @raylib.is_key_down(key_to_raylib(code)) {
pressed_keys.add(code)
} else {
pressed_keys.remove(code)
}
}
}
}
///|
fn sync_mouse_events() -> Unit {
if backend.mouse_state is Some(mouse) &&
backend.mouse_relative_delta is Some(relative_delta) {
let pos = @raylib.get_mouse_position() |> to_selene_vec2
let delta = @raylib.get_mouse_delta() |> to_selene_vec2
let wheel = @raylib.get_mouse_wheel_move_v() |> to_selene_vec2
// Keep raw screen-space mouse coordinates consistent with canvas backend.
// Core click systems are responsible for applying zoom conversion.
mouse.pos = Vec2(pos[X], pos[Y])
relative_delta.val = Some(Vec2(delta[X], delta[Y]))
if backend.mouse_wheel is Some(mouse_wheel) {
mouse_wheel.delta = Vec2(wheel[X], wheel[Y])
}
mouse.left_button = @raylib.is_mouse_button_down(@raylib.MouseButtonLeft)
mouse.right_button = @raylib.is_mouse_button_down(@raylib.MouseButtonRight)
mouse.middle_button = @raylib.is_mouse_button_down(
@raylib.MouseButtonMiddle,
)
}
}
///|
fn push_raylib_touch_change(
changes : Array[@inputs.TouchEvent],
id : @inputs.TouchId,
phase : @inputs.TouchPhase,
position : @smath.Vec2,
previous_position : @smath.Vec2,
primary : Bool,
) -> Unit {
changes.push({
id,
source: PointerTouch,
phase,
position,
previous_position,
delta: position - previous_position,
primary,
})
}
///|
fn sync_touch_events() -> Unit {
guard backend.touch_state is Some(touches) else { return }
guard backend.touch_changes_state is Some(changes) else { return }
changes.clear()
let current_ids : Set[@inputs.TouchId] = Set([])
let count = @raylib.get_touch_point_count()
for index in 0.. to_selene_vec2
let primary = index == 0
current_ids.add(id)
let previous_position = backend.last_touches
.get(id)
.map_or(position, fn(touch) { touch.position })
let delta = position - previous_position
touches.set(id, {
id,
source: PointerTouch,
position,
previous_position,
delta,
primary,
})
if backend.last_touches.contains(id) {
if delta[X] != 0.0 || delta[Y] != 0.0 {
push_raylib_touch_change(
changes,
id,
Moved,
position,
previous_position,
primary,
)
}
} else {
push_raylib_touch_change(
changes,
id,
Started,
position,
previous_position,
primary,
)
}
}
for id, touch in backend.last_touches {
if !current_ids.contains(id) {
push_raylib_touch_change(
changes,
id,
Ended,
touch.position,
touch.position,
touch.primary,
)
ignore(touches.remove(id))
}
}
backend.last_touches.clear()
for id, touch in touches {
backend.last_touches.set(id, touch)
}
}
///|
const MAX_GAMEPADS : Int = 8
///|
fn sync_gamepad_events() -> Unit {
guard backend.gamepads_state is Some(gamepads) else { return }
guard backend.gamepad_buttons_state is Some(buttons) else { return }
guard backend.gamepad_axes_state is Some(axes) else { return }
gamepads.clear()
buttons.clear()
axes.clear()
for id in 0.. Unit {
let mappings : Array[(Int, @inputs.GamepadButton)] = [
(@raylib.GamepadButtonRightFaceDown, South),
(@raylib.GamepadButtonRightFaceRight, East),
(@raylib.GamepadButtonRightFaceUp, North),
(@raylib.GamepadButtonRightFaceLeft, West),
(@raylib.GamepadButtonLeftTrigger1, LeftTrigger),
(@raylib.GamepadButtonLeftTrigger2, LeftTrigger2),
(@raylib.GamepadButtonRightTrigger1, RightTrigger),
(@raylib.GamepadButtonRightTrigger2, RightTrigger2),
(@raylib.GamepadButtonMiddleLeft, Select),
(@raylib.GamepadButtonMiddleRight, Start),
(@raylib.GamepadButtonMiddle, Mode),
(@raylib.GamepadButtonLeftThumb, LeftThumb),
(@raylib.GamepadButtonRightThumb, RightThumb),
(@raylib.GamepadButtonLeftFaceUp, DPadUp),
(@raylib.GamepadButtonLeftFaceDown, DPadDown),
(@raylib.GamepadButtonLeftFaceLeft, DPadLeft),
(@raylib.GamepadButtonLeftFaceRight, DPadRight),
]
for mapping in mappings {
if @raylib.is_gamepad_button_down(id, mapping.0) {
buttons.add(GamepadButtonInput(gamepad, mapping.1))
}
}
}
///|
fn sync_one_gamepad_axes(
id : Int,
gamepad : @inputs.Gamepad,
axes : Map[@inputs.GamepadAxisInput, Double],
) -> Unit {
axes.set(
GamepadAxisInput(gamepad, LeftStickX),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisLeftX).to_double(),
)
axes.set(
GamepadAxisInput(gamepad, LeftStickY),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisLeftY).to_double(),
)
axes.set(
GamepadAxisInput(gamepad, RightStickX),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisRightX).to_double(),
)
axes.set(
GamepadAxisInput(gamepad, RightStickY),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisRightY).to_double(),
)
axes.set(
GamepadAxisInput(gamepad, LeftZ),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisLeftTrigger).to_double(),
)
axes.set(
GamepadAxisInput(gamepad, RightZ),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisRightTrigger).to_double(),
)
}
///|
fn sync_mouse_lock() -> Unit {
if backend.lock_state is Some(lock_state) {
// Keep requested lock state separate from runtime cursor visibility.
if lock_state.val != backend.cursor_locked {
backend.lock_requested = lock_state.val
}
if backend.lock_requested {
if !backend.cursor_locked && @raylib.is_window_focused() {
@raylib.disable_cursor()
}
} else if backend.cursor_locked {
@raylib.enable_cursor()
}
backend.cursor_locked = @raylib.is_cursor_hidden()
lock_state.val = backend.cursor_locked
}
}
///|
fn update_audio_instances() -> Unit {
for _instance, playback in backend.audio_instances {
tick_audio_playback(playback)
}
for pair in backend.audio_instances.to_array() {
if pair.1.finished {
free_audio_playback(pair.1)
backend.audio_instances.remove(pair.0)
}
}
}
///|
fn update_text_raster_scale() -> Unit {
let screen_width = @raylib.get_screen_width()
let screen_height = @raylib.get_screen_height()
let render_width = @raylib.get_render_width()
let render_height = @raylib.get_render_height()
let scale_x = if screen_width > 0 && render_width > 0 {
render_width.to_double() / screen_width.to_double()
} else {
1.0
}
let scale_y = if screen_height > 0 && render_height > 0 {
render_height.to_double() / screen_height.to_double()
} else {
1.0
}
let next = @cmp.maximum(1.0, @cmp.maximum(scale_x, scale_y))
if (next - backend.text_raster_scale).abs() <= 0.00001 {
return
}
for _key, entry in backend.cosmic_textures {
@raylib.unload_texture(entry.texture)
}
backend.cosmic_textures.clear()
cosmic_swash_cache.val = @cosmic.SwashCache::new()
backend.text_raster_scale = next
}
///|
fn cleanup_resources() -> Unit {
for pair in backend.audio_instances.to_array() {
free_audio_playback(pair.1)
}
backend.audio_instances.clear()
for _path, sound in backend.sounds {
@raylib.unload_sound(sound)
}
backend.sounds.clear()
for _path, font in backend.fonts_by_path {
@raylib.unload_font(font)
}
backend.fonts_by_path.clear()
backend.font_alias.clear()
reset_cosmic_runtime()
backend.embedded_asset_cache.clear()
for _key, mesh in backend.primitive_meshes {
@raylib.unload_mesh(mesh)
}
backend.primitive_meshes.clear()
for _key, mesh in backend.triangle_mesh_cache {
@raylib.unload_mesh(mesh)
}
backend.triangle_mesh_cache.clear()
let cleanup_default_material = if backend.lit_material is Some(_) ||
backend.lit_textured_material is Some(_) ||
backend.shadow_material is Some(_) {
Some(@raylib.Material::default())
} else {
None
}
if backend.lit_material is Some(material) {
if cleanup_default_material is Some(default_material) {
reset_material_from_material(material, default_material)
}
@raylib.unload_material(material)
}
backend.lit_material = None
if backend.lit_textured_material is Some(material) {
if cleanup_default_material is Some(default_material) {
reset_material_from_material(material, default_material)
}
@raylib.unload_material(material)
}
backend.lit_textured_material = None
if backend.shadow_material is Some(material) {
if cleanup_default_material is Some(default_material) {
reset_material_from_material(material, default_material)
}
@raylib.unload_material(material)
}
backend.shadow_material = None
if cleanup_default_material is Some(material) {
@raylib.unload_material(material)
}
if backend.lighting_shader is Some(state) {
@raylib.unload_shader(state.shader)
}
backend.lighting_shader = None
if backend.shadow_shader is Some(state) {
@raylib.unload_shader(state.shader)
}
backend.shadow_shader = None
if backend.image_material_shader is Some(state) {
@raylib.unload_shader(state.shader)
}
backend.image_material_shader = None
for _path, texture in backend.textures {
@raylib.unload_texture(texture)
}
backend.textures.clear()
for render_texture in backend.shadow_render_textures {
render_texture.unload()
}
backend.shadow_render_textures.clear()
backend.directional_shadow_tile_sizes.clear()
backend.directional_shadow_atlas_columns.clear()
backend.directional_shadow_atlas_rows.clear()
if backend.spot_shadow_render_texture is Some(render_texture) {
render_texture.unload()
}
backend.spot_shadow_render_texture = None
backend.spot_shadow_map_size = 0
if backend.point_shadow_render_texture is Some(render_texture) {
render_texture.unload()
}
backend.point_shadow_render_texture = None
backend.point_shadow_map_size = 0
for _name, target in backend.offscreen_targets_2d {
target.render_texture.unload()
}
backend.offscreen_targets_2d.clear()
backend.active_2d_offscreen_target = None
backend.meshes3d.clear()
backend.materials3d.clear()
backend.images3d.clear()
backend.texture_warning_keys.clear()
}
///|
pub fn initialize(
config : @runtime.WindowConfig,
callbacks : @runtime.RunnerCallbacks,
) -> () -> Unit {
ignore(config.viewport_width)
ignore(config.viewport_height)
backend.default_image_sampler = config.default_image_sampler
backend.close_requested = false
fn() {
@raylib.set_config_flags(@raylib.FlagWindowHighdpi)
@raylib.init_window(
config.screen_width.to_int(),
config.screen_height.to_int(),
"Selene (raylib)",
)
@raylib.set_exit_key(@raylib.KeyNull)
@raylib.set_target_fps(config.fps.reinterpret_as_int())
if !@raylib.is_audio_device_ready() {
@raylib.init_audio_device()
}
@raylib.set_audio_stream_buffer_size_default(
RAYLIB_DEFAULT_AUDIO_STREAM_BUFFER_SIZE,
)
backend.texture_warning_keys.clear()
(callbacks.startup)()
while !backend.close_requested && !@raylib.window_should_close() {
let delta = @raylib.get_frame_time().to_double()
backend.realtime_delta = delta
sync_key_events()
sync_mouse_events()
sync_touch_events()
sync_gamepad_events()
sync_mouse_lock()
(callbacks.service_frame)()
(callbacks.game_loop)(delta * backend.time_scale)
@raylib.begin_drawing()
update_text_raster_scale()
@raylib.clear_background(@raylib.black)
backend.frame_has_draw_commands = false
(callbacks.render_loop)(delta * backend.time_scale)
(callbacks.service_frame)()
@raylib.end_drawing()
}
cleanup_resources()
if @raylib.is_audio_device_ready() {
@raylib.close_audio_device()
}
@raylib.close_window()
}
}
///|
pub fn draw_image(
command : @render2d_types.ImageDrawCommand2D,
image_path : String,
sampler : @asset_types.ImageSampler,
) -> Unit {
let texture = match offscreen_target_texture(image_path) {
Some(texture) => Some(texture)
None =>
if offscreen_target_name_from_path(image_path) is Some(_) {
None
} else {
Some(get_texture(image_path, sampler~))
}
}
guard texture is Some(texture) else { return }
mark_frame_drawn()
with_blend_mode(command.blend_mode, fn() {
draw_image_repeat_transform(
texture,
command,
flip_y=offscreen_target_name_from_path(image_path) is Some(_),
)
})
}
///|
pub fn draw_image_material(
command : @render2d_types.ImageMaterialDrawCommand2D,
image_path : String,
sampler : @asset_types.ImageSampler,
) -> Unit {
let texture = match offscreen_target_texture(image_path) {
Some(texture) => Some(texture)
None =>
if offscreen_target_name_from_path(image_path) is Some(_) {
None
} else {
Some(get_texture(image_path, sampler~))
}
}
guard texture is Some(texture) else { return }
mark_frame_drawn()
with_blend_mode(command.image.blend_mode, fn() {
draw_image_material_quad(
texture,
command,
offscreen_target_name_from_path(image_path) is Some(_),
)
})
}