Allow OHOS servoshell to have a simple multiple tab implementation. (#36891)

Currently we just pause the compositor and replace the window in it
while having separate bookkeeping to remember which window belongs to
which tab.
Currently there are no tests for OHOS, so we cannot test the changes.

---------

Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>
Co-authored-by: Jonathan Schwender <55576758+jschwe@users.noreply.github.com>
This commit is contained in:
Narfinger 2025-06-16 10:17:31 +02:00 committed by GitHub
parent 71bf9fb92d
commit 3b73b83a9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 245 additions and 87 deletions

View file

@ -292,6 +292,10 @@ impl TouchHandler {
.expect("Current Touch sequence does not exist") .expect("Current Touch sequence does not exist")
} }
fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
self.touch_sequence_map.get_mut(&self.current_sequence_id)
}
pub(crate) fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo { pub(crate) fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
self.touch_sequence_map self.touch_sequence_map
.get(&sequence_id) .get(&sequence_id)
@ -374,7 +378,12 @@ impl TouchHandler {
id: TouchId, id: TouchId,
point: Point2D<f32, DevicePixel>, point: Point2D<f32, DevicePixel>,
) -> TouchMoveAction { ) -> TouchMoveAction {
let touch_sequence = self.get_current_touch_sequence_mut(); // As `TouchHandler` is per `WebViewRenderer` which is per `WebView` we might get a Touch Sequence Move that
// started with a down on a different webview. As the touch_sequence id is only changed on touch_down this
// move event gets a touch id which is already cleaned up.
let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
return TouchMoveAction::NoAction;
};
let idx = match touch_sequence let idx = match touch_sequence
.active_touch_points .active_touch_points
.iter_mut() .iter_mut()
@ -529,7 +538,10 @@ impl TouchHandler {
} }
pub fn on_touch_cancel(&mut self, id: TouchId, _point: Point2D<f32, DevicePixel>) { pub fn on_touch_cancel(&mut self, id: TouchId, _point: Point2D<f32, DevicePixel>) {
let touch_sequence = self.get_current_touch_sequence_mut(); // A similar thing with touch move can happen here where the event is coming from a different webview.
let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
return;
};
match touch_sequence match touch_sequence
.active_touch_points .active_touch_points
.iter() .iter()

View file

@ -182,6 +182,7 @@ pub enum TouchEventType {
pub struct TouchId(pub i32); pub struct TouchId(pub i32);
/// An ID for a sequence of touch events between a `Down` and the `Up` or `Cancel` event. /// An ID for a sequence of touch events between a `Down` and the `Up` or `Cancel` event.
/// The ID is the same for all events between `Down` and `Up` or `Cancel`
#[repr(transparent)] #[repr(transparent)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct TouchSequenceId(u32); pub struct TouchSequenceId(u32);

View file

@ -356,6 +356,15 @@ impl RunningAppState {
self.inner_mut().webviews.insert(webview.id(), webview); self.inner_mut().webviews.insert(webview.id(), webview);
} }
/// The focused webview will not be immediately valid via `active_webview()`
pub(crate) fn focus_webview(&self, id: WebViewId) {
if let Some(webview) = self.inner().webviews.get(&id) {
webview.focus();
} else {
error!("We could not find the webview with this id {id}");
}
}
fn inner(&self) -> Ref<RunningAppStateInner> { fn inner(&self) -> Ref<RunningAppStateInner> {
self.inner.borrow() self.inner.borrow()
} }
@ -372,14 +381,14 @@ impl RunningAppState {
Ok(webview_id) Ok(webview_id)
} }
fn newest_webview(&self) -> Option<WebView> { pub(crate) fn newest_webview(&self) -> Option<WebView> {
self.inner() self.inner()
.creation_order .creation_order
.last() .last()
.and_then(|id| self.inner().webviews.get(id).cloned()) .and_then(|id| self.inner().webviews.get(id).cloned())
} }
fn active_webview(&self) -> WebView { pub(crate) fn active_webview(&self) -> WebView {
self.inner() self.inner()
.focused_webview_id .focused_webview_id
.and_then(|id| self.inner().webviews.get(&id).cloned()) .and_then(|id| self.inner().webviews.get(&id).cloned())

View file

@ -6,8 +6,9 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use std::os::raw::c_void; use std::os::raw::c_void;
use std::rc::Rc;
use std::sync::mpsc::{Receiver, Sender}; use std::sync::mpsc::{Receiver, Sender};
use std::sync::{LazyLock, Once, OnceLock, mpsc}; use std::sync::{LazyLock, Mutex, Once, OnceLock, mpsc};
use std::thread; use std::thread;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
@ -23,7 +24,7 @@ use ohos_ime_sys::types::InputMethod_EnterKeyType;
use servo::style::Zero; use servo::style::Zero;
use servo::{ use servo::{
AlertResponse, EventLoopWaker, InputMethodType, LoadStatus, MediaSessionPlaybackState, AlertResponse, EventLoopWaker, InputMethodType, LoadStatus, MediaSessionPlaybackState,
PermissionRequest, SimpleDialog, WebView, PermissionRequest, SimpleDialog, WebView, WebViewId,
}; };
use xcomponent_sys::{ use xcomponent_sys::{
OH_NativeXComponent, OH_NativeXComponent_Callback, OH_NativeXComponent_GetKeyEvent, OH_NativeXComponent, OH_NativeXComponent_Callback, OH_NativeXComponent_GetKeyEvent,
@ -65,9 +66,11 @@ fn call(action: ServoAction) -> Result<(), CallError> {
} }
#[repr(transparent)] #[repr(transparent)]
struct XComponentWrapper(*mut OH_NativeXComponent); #[derive(Clone)]
pub(crate) struct XComponentWrapper(*mut OH_NativeXComponent);
#[repr(transparent)] #[repr(transparent)]
struct WindowWrapper(*mut c_void); #[derive(Clone)]
pub(crate) struct WindowWrapper(*mut c_void);
unsafe impl Send for XComponentWrapper {} unsafe impl Send for XComponentWrapper {}
unsafe impl Send for WindowWrapper {} unsafe impl Send for WindowWrapper {}
@ -80,7 +83,6 @@ pub(super) enum TouchEventType {
Unknown, Unknown,
} }
#[derive(Debug)]
pub(super) enum ServoAction { pub(super) enum ServoAction {
WakeUp, WakeUp,
LoadUrl(String), LoadUrl(String),
@ -104,6 +106,8 @@ pub(super) enum ServoAction {
width: i32, width: i32,
height: i32, height: i32,
}, },
FocusWebview(u32),
NewWebview(XComponentWrapper, WindowWrapper),
} }
/// Queue length for the thread-safe function to submit URL updates to ArkTS /// Queue length for the thread-safe function to submit URL updates to ArkTS
@ -125,6 +129,20 @@ static PROMPT_TOAST: OnceLock<
ThreadsafeFunction<String, (), String, false, false, PROMPT_QUEUE_SIZE>, ThreadsafeFunction<String, (), String, false, false, PROMPT_QUEUE_SIZE>,
> = OnceLock::new(); > = OnceLock::new();
/// Storing webview related items
struct NativeWebViewComponents {
/// The id of the related webview
id: WebViewId,
/// The XComponentWrapper for the above webview
xcomponent: XComponentWrapper,
/// The WindowWrapper for the above webview
window: WindowWrapper,
}
/// Currently we do not support different contexts for different windows but we might want to change tabs.
/// For this we store the window context for every tab and change the compositor by hand.
static NATIVE_WEBVIEWS: Mutex<Vec<NativeWebViewComponents>> = Mutex::new(Vec::new());
impl ServoAction { impl ServoAction {
fn dispatch_touch_event( fn dispatch_touch_event(
servo: &RunningAppState, servo: &RunningAppState,
@ -143,7 +161,7 @@ impl ServoAction {
} }
// todo: consider making this take `self`, so we don't need to needlessly clone. // todo: consider making this take `self`, so we don't need to needlessly clone.
fn do_action(&self, servo: &RunningAppState) { fn do_action(&self, servo: &Rc<RunningAppState>) {
use ServoAction::*; use ServoAction::*;
match self { match self {
WakeUp => servo.perform_updates(), WakeUp => servo.perform_updates(),
@ -155,7 +173,7 @@ impl ServoAction {
x, x,
y, y,
pointer_id, pointer_id,
} => Self::dispatch_touch_event(servo, *kind, *x, *y, *pointer_id), } => Self::dispatch_touch_event(&servo, *kind, *x, *y, *pointer_id),
KeyUp(k) => servo.key_up(k.clone()), KeyUp(k) => servo.key_up(k.clone()),
KeyDown(k) => servo.key_down(k.clone()), KeyDown(k) => servo.key_down(k.clone()),
InsertText(text) => servo.ime_insert_text(text.clone()), InsertText(text) => servo.ime_insert_text(text.clone()),
@ -183,6 +201,56 @@ impl ServoAction {
servo.present_if_needed(); servo.present_if_needed();
}, },
Resize { width, height } => servo.resize(Coordinates::new(0, 0, *width, *height)), Resize { width, height } => servo.resize(Coordinates::new(0, 0, *width, *height)),
FocusWebview(arkts_id) => {
if let Some(native_webview_components) =
NATIVE_WEBVIEWS.lock().unwrap().get(*arkts_id as usize)
{
if (servo.active_webview().id() != native_webview_components.id) {
servo.focus_webview(native_webview_components.id);
servo.pause_compositor();
let (window_handle, _, coordinates) = simpleservo::get_raw_window_handle(
native_webview_components.xcomponent.0,
native_webview_components.window.0,
);
servo.resume_compositor(window_handle, coordinates);
let url = servo
.active_webview()
.url()
.map(|u| u.to_string())
.unwrap_or(String::from("about:blank"));
SET_URL_BAR_CB
.get()
.map(|f| f.call(url, ThreadsafeFunctionCallMode::Blocking));
}
} else {
error!("Could not find webview to focus");
}
},
NewWebview(xcomponent, window) => {
servo.pause_compositor();
servo.new_toplevel_webview("about:blank".parse().unwrap());
let (window_handle, _, coordinates) =
simpleservo::get_raw_window_handle(xcomponent.0, window.0);
servo.resume_compositor(window_handle, coordinates);
let webview = servo.newest_webview().expect("There should always be one");
let id = webview.id();
NATIVE_WEBVIEWS
.lock()
.unwrap()
.push(NativeWebViewComponents {
id: id,
xcomponent: xcomponent.clone(),
window: window.clone(),
});
let url = webview
.url()
.map(|u| u.to_string())
.unwrap_or(String::from("about:blank"));
SET_URL_BAR_CB
.get()
.map(|f| f.call(url, ThreadsafeFunctionCallMode::Blocking));
},
}; };
} }
} }
@ -221,50 +289,64 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window
let xc_wrapper = XComponentWrapper(xcomponent); let xc_wrapper = XComponentWrapper(xcomponent);
let window_wrapper = WindowWrapper(window); let window_wrapper = WindowWrapper(window);
// Todo: Perhaps it would be better to move this thread into the vsync signal thread. if !SERVO_CHANNEL.get().is_some() {
// This would allow us to save one thread and the IPC for the vsync signal. // Todo: Perhaps it would be better to move this thread into the vsync signal thread.
// // This would allow us to save one thread and the IPC for the vsync signal.
// Each thread will send its id via the channel //
let _main_surface_thread = thread::spawn(move || { // Each thread will send its id via the channel
let (tx, rx): (Sender<ServoAction>, Receiver<ServoAction>) = mpsc::channel(); let _main_surface_thread = thread::spawn(move || {
let (tx, rx): (Sender<ServoAction>, Receiver<ServoAction>) = mpsc::channel();
SERVO_CHANNEL SERVO_CHANNEL
.set(tx.clone()) .set(tx.clone())
.expect("Servo channel already initialized"); .expect("Servo channel already initialized");
let wakeup = Box::new(WakeupCallback::new(tx)); let wakeup = Box::new(WakeupCallback::new(tx));
let callbacks = Box::new(HostCallbacks::new()); let callbacks = Box::new(HostCallbacks::new());
let xc = xc_wrapper; let xc = xc_wrapper;
let window = window_wrapper; let window = window_wrapper;
let init_opts = if let Ok(ServoAction::Initialize(init_opts)) = rx.recv() {
init_opts
} else {
panic!("Servos GL thread received another event before it was initialized")
};
let servo = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks)
.expect("Servo initialization failed");
info!("Surface created!"); let init_opts = if let Ok(ServoAction::Initialize(init_opts)) = rx.recv() {
let native_vsync = init_opts
ohos_vsync::NativeVsync::new("ServoVsync").expect("Failed to create NativeVsync"); } else {
// get_period() returns an error - perhaps we need to wait until the first callback? panic!("Servos GL thread received another event before it was initialized")
// info!("Native vsync period is {} nanoseconds", native_vsync.get_period().unwrap()); };
unsafe { let servo = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks)
native_vsync .expect("Servo initialization failed");
.request_raw_callback_with_self(Some(on_vsync_cb))
.expect("Failed to request vsync callback")
}
info!("Enabled Vsync!");
while let Ok(action) = rx.recv() { NATIVE_WEBVIEWS
trace!("Wakeup message received!"); .lock()
action.do_action(&servo); .unwrap()
} .push(NativeWebViewComponents {
id: servo.active_webview().id(),
xcomponent: xc,
window,
});
info!("Sender disconnected - Terminating main surface thread"); info!("Surface created!");
}); let native_vsync =
ohos_vsync::NativeVsync::new("ServoVsync").expect("Failed to create NativeVsync");
// get_period() returns an error - perhaps we need to wait until the first callback?
// info!("Native vsync period is {} nanoseconds", native_vsync.get_period().unwrap());
unsafe {
native_vsync
.request_raw_callback_with_self(Some(on_vsync_cb))
.expect("Failed to request vsync callback")
}
info!("Enabled Vsync!");
while let Ok(action) = rx.recv() {
trace!("Wakeup message received!");
action.do_action(&servo);
}
info!("Sender disconnected - Terminating main surface thread");
});
} else {
call(ServoAction::NewWebview(xc_wrapper, window_wrapper))
.expect("Could not create new webview");
}
info!("Returning from on_surface_created_cb"); info!("Returning from on_surface_created_cb");
} }
@ -647,6 +729,12 @@ pub fn init_servo(init_opts: InitOpts) -> napi_ohos::Result<()> {
Ok(()) Ok(())
} }
#[napi]
fn focus_webview(id: u32) {
debug!("Focusing webview {id} from napi");
call(ServoAction::FocusWebview(id)).expect("Could not focus webview");
}
struct OhosImeOptions { struct OhosImeOptions {
input_type: ohos_ime_sys::types::InputMethod_TextInputType, input_type: ohos_ime_sys::types::InputMethod_TextInputType,
enterkey_type: InputMethod_EnterKeyType, enterkey_type: InputMethod_EnterKeyType,

View file

@ -26,6 +26,20 @@ use crate::egl::ohos::InitOpts;
use crate::egl::ohos::resources::ResourceReaderInstance; use crate::egl::ohos::resources::ResourceReaderInstance;
use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments}; use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments};
pub(crate) fn get_raw_window_handle(
xcomponent: *mut OH_NativeXComponent,
window: *mut c_void,
) -> (RawWindowHandle, euclid::default::Size2D<i32>, Coordinates) {
let window_size = unsafe { super::get_xcomponent_size(xcomponent, window) }
.expect("Could not get native window size");
let (x, y) = unsafe { super::get_xcomponent_offset(xcomponent, window) }
.expect("Could not get native window offset");
let coordinates = Coordinates::new(x, y, window_size.width, window_size.height);
let native_window = NonNull::new(window).expect("Could not get native window");
let window_handle = RawWindowHandle::OhosNdk(OhosNdkWindowHandle::new(native_window));
(window_handle, window_size, coordinates)
}
#[derive(Debug)] #[derive(Debug)]
struct NativeValues { struct NativeValues {
cache_dir: String, cache_dir: String,
@ -143,19 +157,12 @@ pub fn init(
#[cfg(target_env = "ohos")] #[cfg(target_env = "ohos")]
crate::egl::ohos::set_log_filter(servoshell_preferences.log_filter.as_deref()); crate::egl::ohos::set_log_filter(servoshell_preferences.log_filter.as_deref());
let Ok(window_size) = (unsafe { super::get_xcomponent_size(xcomponent, native_window) }) else { let (window_handle, window_size, coordinates) =
return Err("Failed to get xcomponent size"); get_raw_window_handle(xcomponent, native_window);
};
let Ok((x, y)) = (unsafe { super::get_xcomponent_offset(xcomponent, native_window) }) else {
return Err("Failed to get xcomponent offset");
};
let coordinates = Coordinates::new(x, y, window_size.width, window_size.height);
let display_handle = RawDisplayHandle::Ohos(OhosDisplayHandle::new()); let display_handle = RawDisplayHandle::Ohos(OhosDisplayHandle::new());
let display_handle = unsafe { DisplayHandle::borrow_raw(display_handle) }; let display_handle = unsafe { DisplayHandle::borrow_raw(display_handle) };
let native_window = NonNull::new(native_window).expect("Could not get native window");
let window_handle = RawWindowHandle::OhosNdk(OhosNdkWindowHandle::new(native_window));
let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) }; let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) };
let rendering_context = Rc::new( let rendering_context = Rc::new(

View file

@ -5,11 +5,18 @@ import promptAction from '@ohos.promptAction';
interface ServoXComponentInterface { interface ServoXComponentInterface {
loadURL(url: string): void; loadURL(url: string): void;
goBack(): void; goBack(): void;
goForward(): void; goForward(): void;
registerURLcallback(callback: (url: string) => void): void; registerURLcallback(callback: (url: string) => void): void;
registerTerminateCallback(callback: () => void): void; registerTerminateCallback(callback: () => void): void;
registerPromptToastCallback(callback: (msg: string) => void): void registerPromptToastCallback(callback: (msg: string) => void): void
focusWebview(index: number):void;
initServo(options: InitOpts): void; initServo(options: InitOpts): void;
} }
@ -20,10 +27,10 @@ interface InitOpts {
} }
function prompt_toast(msg: string) { function prompt_toast(msg: string) {
promptAction.showToast({ promptAction.showToast({
message: msg, message: msg,
duration: 2000 duration: 2000
}); });
} }
// Use the getShared API to obtain the LocalStorage instance shared by stage. // Use the getShared API to obtain the LocalStorage instance shared by stage.
@ -38,11 +45,12 @@ struct Index {
type: XComponentType.SURFACE, type: XComponentType.SURFACE,
libraryname: 'servoshell', libraryname: 'servoshell',
} }
private context = getContext(this) as common.UIAbilityContext; private context = getContext(this) as common.UIAbilityContext;
@LocalStorageProp('InitialURI') InitialURI: string = "unused" @LocalStorageProp('InitialURI') InitialURI: string = "unused"
@LocalStorageProp('CommandlineArgs') CommandlineArgs: string = "" @LocalStorageProp('CommandlineArgs') CommandlineArgs: string = ""
@State urlToLoad: string = this.InitialURI @State urlToLoad: string = this.InitialURI
@State tablist: Array<number> = [];
@State currentIndex: number = 0;
// Called when the user swipes from the right or left edge to the middle // Called when the user swipes from the right or left edge to the middle
// Default behavior is bringing the app to the background. // Default behavior is bringing the app to the background.
@ -57,52 +65,85 @@ struct Index {
// Flex. // Flex.
Flex({ direction: FlexDirection.Column}) { Flex({ direction: FlexDirection.Column}) {
Row() { Row() {
Button('⇦').backgroundColor(Color.White) Button('+')
.backgroundColor(Color.White)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bolder)
.fontSize(22)
.width('12%')
.onClick((event) => {
if (this.tablist.length==0) {
this.tablist.push(2);
} else {
this.tablist.push(this.tablist[this.tablist.length-1]+1);
}
// yes this is correct as we always have one tab extra
// The tab extra is seperate for the initialization and will always exist.
// It is not in the tablist.
this.currentIndex = this.tablist.length;
})
Button('⇦')
.backgroundColor(Color.White)
.fontColor(Color.Black) .fontColor(Color.Black)
.fontWeight(FontWeight.Bolder) .fontWeight(FontWeight.Bolder)
.width('12%') .width('12%')
.fontSize(12) .fontSize(12)
.onClick(()=>{ .onClick(() => {
this.onBackPress() this.onBackPress()
}) })
Button('⇨').backgroundColor(Color.White) Button('⇨')
.backgroundColor(Color.White)
.fontColor(Color.Black) .fontColor(Color.Black)
.fontWeight(FontWeight.Bolder) .fontWeight(FontWeight.Bolder)
.fontSize(12) .fontSize(12)
.width('12%') .width('12%')
.onClick(()=> { .onClick(() => {
this.xComponentContext?.goForward() this.xComponentContext?.goForward()
}) })
TextInput({placeholder:'URL',text: $$this.urlToLoad}) TextInput({ placeholder: 'URL', text: $$this.urlToLoad })
.type(InputType.Normal) .type(InputType.Normal)
.width('76%') .width('76%')
.onChange((value) => { .onChange((value) => {
this.urlToLoad = value this.urlToLoad = value
}) })
.onSubmit((EnterKeyType)=>{ .onSubmit((EnterKeyType) => {
this.xComponentContext?.loadURL(this.urlToLoad) this.xComponentContext?.loadURL(this.urlToLoad)
console.info('Load URL: ', this.urlToLoad) console.info('Load URL: ', this.urlToLoad)
}) })
} }
XComponent(this.xComponentAttrs)
.focusable(true) Tabs({ barPosition: BarPosition.Start, index: this.currentIndex}) {
.onLoad((xComponentContext) => { TabContent() {
this.xComponentContext = xComponentContext as ServoXComponentInterface; XComponent(this.xComponentAttrs)
let resource_dir: string = this.context.resourceDir; .focusable(true)
console.debug("resourceDir: ", resource_dir); .onLoad((xComponentContext) => {
let init_options: InitOpts = { this.xComponentContext = xComponentContext as ServoXComponentInterface;
url: this.urlToLoad, let resource_dir: string = this.context.resourceDir;
resourceDir: resource_dir, let cache_dir: string = this.context.cacheDir;
commandlineArgs: this.CommandlineArgs console.debug("resourceDir: ", resource_dir);
} console.debug("cacheDir: ", cache_dir);
this.xComponentContext.initServo(init_options) let init_options: InitOpts = {
this.xComponentContext.registerURLcallback((new_url) => { url: this.urlToLoad,
console.info('New URL from native: ', new_url) resourceDir: resource_dir,
this.urlToLoad = new_url commandlineArgs: this.CommandlineArgs
}) }
this.xComponentContext.registerTerminateCallback(() => { this.context?.terminateSelf(); }) this.xComponentContext.initServo(init_options)
this.xComponentContext.registerPromptToastCallback(prompt_toast) this.xComponentContext.registerURLcallback((new_url) => {
console.info('New URL from native: ', new_url)
this.urlToLoad = new_url
})
this.xComponentContext.registerPromptToastCallback(prompt_toast)
})
}.tabBar('1')
ForEach(this.tablist, (item: number) => {
TabContent() {
XComponent(this.xComponentAttrs)
.focusable(true)
}.tabBar(String(item))
}) })
}.onChange((index: number) => {
this.xComponentContext?.focusWebview(index);
})
} }
.width('100%') .width('100%')
} }
@ -112,4 +153,4 @@ interface XComponentAttrs {
id: string; id: string;
type: number; type: number;
libraryname: string; libraryname: string;
} }