blob: 3bbffda818023c27e8a5f7cdfed736b60e044607 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Compact native rows with local selection, scrolling and hover controls.
//! Products supply row/action identities; this component knows no business model.
use crate::{appearance::Color, shortcut::Shortcut};
use maka_plugin_core::contract::{Contract, Interface, Transport};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RowsProps {
#[serde(default)]
pub terminal: Option<crate::scene::terminal::Palette>,
pub revision: u64,
/// Select this row on keyboard entry. None retains the current selection,
/// or selects the first row when there is no current selection.
pub entry: Option<String>,
pub rows: Vec<Row>,
pub shortcuts: Vec<Shortcut>,
#[schemars(range(min = 1))]
pub max_rows: u16,
/// Optional smaller row cap below the nearest declared layout viewport's
/// height, in cells; defaults to the surface when no viewport is declared.
#[serde(default)]
pub compact_below: Option<CompactRows>,
#[schemars(range(min = -1, max = 32767))]
pub tab_index: i16,
pub foreground: Color,
pub muted: Color,
pub selected: Color,
pub hovered: Color,
pub scrollbar: RowScrollbar,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CompactRows {
#[schemars(range(min = 1))]
pub height: u16,
#[schemars(range(min = 1))]
pub max_rows: u16,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RowScrollbar {
pub thumb: char,
pub track: char,
pub thumb_color: Color,
pub track_color: Color,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Row {
#[schemars(length(min = 1))]
pub key: String,
pub text: String,
pub actions: Vec<RowAction>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RowAction {
#[schemars(length(min = 1))]
pub key: String,
pub label: String,
pub enabled: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RowActivated {
pub revision: u64,
pub row: String,
pub action: String,
}
impl RowsProps {
pub fn validate(&self) -> crate::Result<()> {
if self
.compact_below
.is_some_and(|compact| compact.max_rows > self.max_rows)
{
return Err(crate::Error::Invalid(
"compact row cap exceeds normal cap".into(),
));
}
let mut rows = BTreeSet::new();
for row in &self.rows {
if !rows.insert(&row.key) {
return Err(crate::Error::Invalid("duplicate row identity".into()));
}
let mut actions = BTreeSet::new();
if row
.actions
.iter()
.any(|action| !actions.insert(&action.key))
{
return Err(crate::Error::Invalid("duplicate row action".into()));
}
}
if self.entry.as_ref().is_some_and(|key| !rows.contains(key)) {
return Err(crate::Error::Invalid("unknown row entry".into()));
}
let mut chords = BTreeSet::new();
// Unlike bubbling command scopes, a native row selector owns printable
// keys. Shortcut shape is validated by the original component contract.
if self
.shortcuts
.iter()
.any(|shortcut| !chords.insert((&shortcut.key, shortcut.physical(false))))
{
return Err(crate::Error::Invalid("duplicate row shortcut".into()));
}
Ok(())
}
}
pub fn activated_schema() -> serde_json::Value {
serde_json::to_value(schemars::schema_for!(RowActivated)).expect("row activation")
}
pub fn rows_contract() -> Contract {
Contract {
name: "maka.ui.Rows".into(),
version: 1,
transport: Transport::Local,
interface: Interface::Component {
props: serde_json::to_value(schemars::schema_for!(RowsProps))
.expect("row props")
.into(),
events: BTreeMap::from([("activated".into(), activated_schema().into())]),
captures: BTreeMap::new(),
},
}
}
pub const IMPLEMENTATION: &str = "maka.ui.rows";
pub fn native() -> maka_plugin_core::package::NativeDefinition {
maka_plugin_core::package::NativeDefinition::new(
IMPLEMENTATION,
serde_json::json!({"type":"object","properties":{},"required":[],"additionalProperties":false}),
[("Rows", rows_contract())],
)
}